Skip to main content

devpage_design/resolve/
mod.rs

1//! Syntax tree → scene: imports and props substituted, every attribute
2//! checked, typed and defaulted.
3
4use std::collections::{BTreeMap, BTreeSet, HashMap};
5use std::rc::Rc;
6use std::sync::Arc;
7
8use peniko::{Blob, ImageAlphaType, ImageData, ImageFormat};
9
10use crate::Problem;
11use crate::fonts::{self, Family, FontFile, FontFiles, FontId};
12use crate::parse::{self, Child, Element, Pos, TextPart, Value};
13use crate::tree::{Children, Ids, NodeId, OrderKey};
14
15/// One color's value: straight-alpha RGBA.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct Rgba {
18    pub r: u8,
19    pub g: u8,
20    pub b: u8,
21    pub a: u8,
22}
23
24/// Which of a design's two themes it is drawn in.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
26pub enum Theme {
27    #[default]
28    Light,
29    Dark,
30}
31
32impl Theme {
33    /// `"light"` or `"dark"` as a theme, as a PNG's `?theme=` names one.
34    pub fn named(name: &str) -> Option<Theme> {
35        match name {
36            "light" => Some(Theme::Light),
37            "dark" => Some(Theme::Dark),
38            _ => None,
39        }
40    }
41}
42
43/// A color in a scene: its value in each theme. A color written as `#…` has
44/// the same value in both; `theme.name` has the themes' two.
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct Color {
47    pub light: Rgba,
48    pub dark: Rgba,
49}
50
51impl Color {
52    /// The same value in both themes.
53    pub const fn fixed(c: Rgba) -> Color {
54        Color { light: c, dark: c }
55    }
56
57    /// The value in `theme`.
58    pub fn get(self, theme: Theme) -> Rgba {
59        match theme {
60            Theme::Light => self.light,
61            Theme::Dark => self.dark,
62        }
63    }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq)]
67pub enum Size {
68    Px(f32),
69    Hug,
70    Fill,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq)]
74pub enum Layout {
75    None,
76    Row,
77    Column,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub enum Align {
82    Start,
83    Center,
84    End,
85    SpaceBetween,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq)]
89pub enum Kind {
90    Frame,
91    Text,
92    Rect,
93    Ellipse,
94    Vector,
95    Image,
96}
97
98/// How a box is filled: one color, or a CSS-style linear gradient.
99#[derive(Debug, Clone, PartialEq)]
100pub enum Fill {
101    Solid(Color),
102    /// `angle` in degrees, CSS's: 0 points up, 90 right, 180 down. Each stop
103    /// is a color at an offset from 0 to 1 along the gradient line.
104    Linear {
105        angle: f32,
106        stops: Vec<(Color, f32)>,
107    },
108}
109
110#[derive(Debug, Clone, Copy, PartialEq)]
111pub enum TextAlign {
112    Left,
113    Center,
114    Right,
115}
116
117/// A `Vector`'s path and the box of path coordinates it is drawn from.
118#[derive(Debug, Clone, PartialEq)]
119pub struct Vector {
120    pub d: String,
121    /// `[minX, minY, width, height]`.
122    pub view_box: [f32; 4],
123}
124
125/// An image file's identity: the SHA-256 of its bytes. Two `Image`s showing
126/// the same bytes — the same file used twice, or two copies of it — share one
127/// [`ImageFile`], decoded once and sent once.
128#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
129pub struct ImageId(pub [u8; 32]);
130
131impl ImageId {
132    pub fn of(bytes: &[u8]) -> ImageId {
133        use sha2::Digest;
134        ImageId(sha2::Sha256::digest(bytes).into())
135    }
136}
137
138impl std::fmt::Debug for ImageId {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        for b in &self.0[..6] {
141            write!(f, "{b:02x}")?;
142        }
143        Ok(())
144    }
145}
146
147/// The most pixels a side an image is drawn from. The GPU keeps every image a
148/// drawing paints in one atlas of [`crate::atlas::SIDE`] pixels a side, so a
149/// larger file is resampled to fit this, keeping its aspect, when it is
150/// decoded — and both backends draw those same pixels.
151pub const MAX_IMAGE_SIDE: u32 = 4096;
152
153/// A decoded image file: its bytes, its own size, and the pixels it is drawn
154/// from — the file's own, or resampled to at most [`MAX_IMAGE_SIDE`] a side.
155#[derive(Debug)]
156pub struct ImageFile {
157    pub id: ImageId,
158    pub bytes: Vec<u8>,
159    /// The file's size in pixels: what an `Image` hugs by.
160    pub width: u32,
161    pub height: u32,
162    /// Straight-alpha RGBA, `pixels_width` × `pixels_height`.
163    pub pixels: Arc<Vec<u8>>,
164    pub pixels_width: u32,
165    pub pixels_height: u32,
166}
167
168impl ImageFile {
169    /// Decodes `bytes`, a PNG or JPEG file, or says why it can't.
170    pub fn new(bytes: Vec<u8>) -> Result<ImageFile, String> {
171        ImageFile::with_id(ImageId::of(&bytes), bytes)
172    }
173
174    /// Decodes `bytes`, already known to be the file `id`.
175    pub fn with_id(id: ImageId, bytes: Vec<u8>) -> Result<ImageFile, String> {
176        let decoded = decode(&bytes)?;
177        let (width, height) = (decoded.width, decoded.height);
178        let (pixels, pixels_width, pixels_height) =
179            fit(decoded.data.data(), width, height, MAX_IMAGE_SIDE);
180        Ok(ImageFile {
181            id,
182            bytes,
183            width,
184            height,
185            pixels: Arc::new(pixels),
186            pixels_width,
187            pixels_height,
188        })
189    }
190
191    /// The pixels as an image for one drawing to paint, under a blob of its
192    /// own that shares the pixels: see `render::Renderer::drawing`.
193    pub fn image_data(&self) -> ImageData {
194        ImageData {
195            data: Blob::new(self.pixels.clone()),
196            format: ImageFormat::Rgba8,
197            alpha_type: ImageAlphaType::Alpha,
198            width: self.pixels_width,
199            height: self.pixels_height,
200        }
201    }
202}
203
204/// `rgba`, `width` × `height`, resampled to at most `max` pixels a side with
205/// its aspect kept, or as it is when it already fits. Each new pixel is the
206/// area-weighted average of the pixels under it, in premultiplied alpha so a
207/// transparent pixel lends no color.
208pub fn fit(rgba: &[u8], width: u32, height: u32, max: u32) -> (Vec<u8>, u32, u32) {
209    if width <= max && height <= max {
210        return (rgba.to_vec(), width, height);
211    }
212    let scale = max as f64 / width.max(height) as f64;
213    let w = ((width as f64 * scale).round() as u32).clamp(1, max);
214    let h = ((height as f64 * scale).round() as u32).clamp(1, max);
215    // For each output index along one axis, the input indices it covers and
216    // how much of each.
217    let weights = |from: u32, to: u32| -> Vec<Vec<(usize, f32)>> {
218        let ratio = from as f64 / to as f64;
219        (0..to)
220            .map(|o| {
221                let (a, b) = (o as f64 * ratio, (o + 1) as f64 * ratio);
222                (a.floor() as usize..(b.ceil() as usize).min(from as usize))
223                    .map(|i| {
224                        let cover = (b.min(i as f64 + 1.0) - a.max(i as f64)) / ratio;
225                        (i, cover as f32)
226                    })
227                    .filter(|&(_, c)| c > 0.0)
228                    .collect()
229            })
230            .collect()
231    };
232    let (across, down) = (weights(width, w), weights(height, h));
233    // Across each row first, premultiplied, then down each column.
234    let mut rows = vec![0f32; 4 * w as usize * height as usize];
235    for y in 0..height as usize {
236        let line = &rgba[4 * y * width as usize..][..4 * width as usize];
237        for (x, ws) in across.iter().enumerate() {
238            let mut sum = [0f32; 4];
239            for &(i, c) in ws {
240                let p = &line[4 * i..][..4];
241                let a = p[3] as f32 * c;
242                sum[0] += p[0] as f32 * a;
243                sum[1] += p[1] as f32 * a;
244                sum[2] += p[2] as f32 * a;
245                sum[3] += a;
246            }
247            rows[4 * (y * w as usize + x)..][..4].copy_from_slice(&sum);
248        }
249    }
250    let mut out = vec![0u8; 4 * w as usize * h as usize];
251    for (y, ws) in down.iter().enumerate() {
252        for x in 0..w as usize {
253            let mut sum = [0f32; 4];
254            for &(i, c) in ws {
255                let p = &rows[4 * (i * w as usize + x)..][..4];
256                for k in 0..4 {
257                    sum[k] += p[k] * c;
258                }
259            }
260            let px = &mut out[4 * (y * w as usize + x)..][..4];
261            if sum[3] > 0.0 {
262                for k in 0..3 {
263                    px[k] = (sum[k] / sum[3]).round().clamp(0.0, 255.0) as u8;
264                }
265                px[3] = sum[3].round().clamp(0.0, 255.0) as u8;
266            }
267        }
268    }
269    (out, w, h)
270}
271
272/// Where an attribute was written, for a problem found only once the design
273/// is laid out and painted. It is never part of what a node draws: two nodes
274/// that differ only in it are equal, and it doesn't travel on the wire.
275#[derive(Debug, Clone)]
276pub struct Origin {
277    pub file: String,
278    pub pos: Pos,
279}
280
281impl Default for Origin {
282    fn default() -> Origin {
283        Origin {
284            file: String::new(),
285            pos: Pos { line: 1, col: 1 },
286        }
287    }
288}
289
290impl PartialEq for Origin {
291    fn eq(&self, _: &Origin) -> bool {
292        true
293    }
294}
295
296/// An `Image`'s file: its address in the served folder and which
297/// [`ImageFile`] in the scene it shows.
298#[derive(Debug, Clone, PartialEq)]
299pub struct Image {
300    pub src: String,
301    pub id: ImageId,
302    /// Where `src` was written.
303    pub at: Origin,
304}
305
306/// A PNG or JPEG file's pixels, as straight-alpha RGBA, or why it has none.
307pub fn decode(data: &[u8]) -> Result<ImageData, String> {
308    let (pixels, width, height) = if data.starts_with(b"\x89PNG\r\n\x1a\n") {
309        let broken = |e: &dyn std::fmt::Display| format!("is a PNG that doesn't decode: {e}");
310        let mut decoder = png::Decoder::new(std::io::Cursor::new(data));
311        decoder.set_transformations(png::Transformations::normalize_to_color8());
312        let mut reader = decoder.read_info().map_err(|e| broken(&e))?;
313        let size = reader
314            .output_buffer_size()
315            .ok_or_else(|| broken(&"too big to hold"))?;
316        let mut buf = vec![0; size];
317        let info = reader.next_frame(&mut buf).map_err(|e| broken(&e))?;
318        buf.truncate(info.buffer_size());
319        let rgba = match info.color_type {
320            png::ColorType::Rgba => buf,
321            png::ColorType::Rgb => buf
322                .chunks(3)
323                .flat_map(|p| [p[0], p[1], p[2], 255])
324                .collect(),
325            png::ColorType::GrayscaleAlpha => buf
326                .chunks(2)
327                .flat_map(|p| [p[0], p[0], p[0], p[1]])
328                .collect(),
329            png::ColorType::Grayscale => buf.iter().flat_map(|&g| [g, g, g, 255]).collect(),
330            // Expanded to RGB by `normalize_to_color8`.
331            png::ColorType::Indexed => return Err(broken(&"an unexpanded palette")),
332        };
333        (rgba, info.width, info.height)
334    } else if data.starts_with(&[0xff, 0xd8, 0xff]) {
335        use zune_jpeg::JpegDecoder;
336        use zune_jpeg::zune_core::colorspace::ColorSpace;
337        use zune_jpeg::zune_core::options::DecoderOptions;
338        let broken = |e: &dyn std::fmt::Display| format!("is a JPEG that doesn't decode: {e}");
339        let options = DecoderOptions::default()
340            .set_strict_mode(true)
341            .jpeg_set_out_colorspace(ColorSpace::RGBA);
342        let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(data), options);
343        let pixels = decoder.decode().map_err(|e| broken(&e))?;
344        let info = decoder.info().ok_or_else(|| broken(&"no header"))?;
345        (pixels, info.width as u32, info.height as u32)
346    } else {
347        return Err("is not a PNG or JPEG file".into());
348    };
349    if width == 0 || height == 0 || pixels.len() != width as usize * height as usize * 4 {
350        return Err(format!(
351            "is an image of {width}×{height} pixels that doesn't decode whole"
352        ));
353    }
354    Ok(ImageData {
355        data: Blob::new(Arc::new(pixels)),
356        format: ImageFormat::Rgba8,
357        alpha_type: ImageAlphaType::Alpha,
358        width,
359        height,
360    })
361}
362
363#[derive(Debug, Clone, PartialEq)]
364pub struct Shadow {
365    pub x: f32,
366    pub y: f32,
367    pub blur: f32,
368    pub spread: f32,
369    pub color: Color,
370    /// Where `shadow` was written.
371    pub at: Origin,
372}
373
374#[derive(Debug, Clone, PartialEq)]
375pub struct TextStyle {
376    /// The font file the text is set in, the face its weight picked; `None`
377    /// for Inter.
378    pub font: Option<FontId>,
379    pub content: String,
380    pub size: f32,
381    pub weight: u16,
382    pub color: Color,
383    pub line_height: f32,
384    pub align: TextAlign,
385}
386
387/// One node of a scene.
388#[derive(Debug, Clone, PartialEq)]
389pub struct Node {
390    /// Where the node came from — see [`NodeId`] — which no edit around it
391    /// changes.
392    pub id: NodeId,
393    pub kind: Kind,
394    pub name: String,
395    pub x: f32,
396    pub y: f32,
397    pub width: Size,
398    pub height: Size,
399    pub opacity: f32,
400    pub on_click: Option<String>,
401    pub fill: Option<Fill>,
402    pub stroke: Option<Color>,
403    pub stroke_width: f32,
404    pub radius: [f32; 4],
405    pub shadow: Option<Shadow>,
406    pub clip: bool,
407    pub layout: Layout,
408    pub gap: f32,
409    pub padding: [f32; 4],
410    pub align: Align,
411    pub justify: Align,
412    pub text: Option<TextStyle>,
413    pub vector: Option<Vector>,
414    pub image: Option<Image>,
415    pub children: Children<Node>,
416}
417
418impl Node {
419    /// Whether two nodes' own attributes are the same, their children aside.
420    pub fn same_own(&self, b: &Node) -> bool {
421        let a = self;
422        a.kind == b.kind
423            && a.name == b.name
424            && a.x == b.x
425            && a.y == b.y
426            && a.width == b.width
427            && a.height == b.height
428            && a.opacity == b.opacity
429            && a.on_click == b.on_click
430            && a.fill == b.fill
431            && a.stroke == b.stroke
432            && a.stroke_width == b.stroke_width
433            && a.radius == b.radius
434            && a.shadow == b.shadow
435            && a.clip == b.clip
436            && a.layout == b.layout
437            && a.gap == b.gap
438            && a.padding == b.padding
439            && a.align == b.align
440            && a.justify == b.justify
441            && a.text == b.text
442            && a.vector == b.vector
443            && a.image == b.image
444    }
445
446    /// The node's own attributes, without its children.
447    pub fn own(&self) -> Node {
448        Node {
449            id: self.id.clone(),
450            kind: self.kind,
451            name: self.name.clone(),
452            x: self.x,
453            y: self.y,
454            width: self.width,
455            height: self.height,
456            opacity: self.opacity,
457            on_click: self.on_click.clone(),
458            fill: self.fill.clone(),
459            stroke: self.stroke,
460            stroke_width: self.stroke_width,
461            radius: self.radius,
462            shadow: self.shadow.clone(),
463            clip: self.clip,
464            layout: self.layout,
465            gap: self.gap,
466            padding: self.padding,
467            align: self.align,
468            justify: self.justify,
469            text: self.text.clone(),
470            vector: self.vector.clone(),
471            image: self.image.clone(),
472            children: Children::new(),
473        }
474    }
475
476    fn new(kind: Kind, name: &str) -> Node {
477        Node {
478            id: NodeId::default(),
479            kind,
480            name: name.to_string(),
481            x: 0.0,
482            y: 0.0,
483            width: Size::Hug,
484            height: Size::Hug,
485            opacity: 1.0,
486            on_click: None,
487            fill: None,
488            stroke: None,
489            stroke_width: 1.0,
490            radius: [0.0; 4],
491            shadow: None,
492            clip: false,
493            layout: Layout::None,
494            gap: 0.0,
495            padding: [0.0; 4],
496            align: Align::Start,
497            justify: Align::Start,
498            text: None,
499            vector: None,
500            image: None,
501            children: Children::new(),
502        }
503    }
504}
505
506/// Reads a design's files by their path from the top of the served folder.
507pub trait Files {
508    /// A file's bytes: a design's text, an `Image`'s file or a font file.
509    fn bytes(&self, path: &str) -> Option<Vec<u8>>;
510    /// The names of the files directly in the folder at `path`, sorted;
511    /// `None` when there is no such folder. A font folder is read this way.
512    fn list(&self, path: &str) -> Option<Vec<String>>;
513    /// A design file's text.
514    fn read(&self, path: &str) -> Option<String> {
515        String::from_utf8(self.bytes(path)?).ok()
516    }
517    /// The design file at `path`, whose text is `source`, parsed. A store
518    /// that keeps files between resolves matches it against the file's last
519    /// parse ([`crate::ids::matched`]), so its elements keep their ids — and
520    /// the nodes they make theirs — across saves.
521    fn parse(&self, path: &str, source: &str) -> Result<Arc<parse::File>, Problem> {
522        let _ = path;
523        parse::parse(source).map(Arc::new)
524    }
525    /// The image file at `path`, decoded, or why it doesn't decode; `None`
526    /// when there is no such file. A store that keeps files between resolves
527    /// keeps their decoded images too, so a save doesn't decode them again.
528    fn image(&self, path: &str) -> Option<Result<Arc<ImageFile>, String>> {
529        self.bytes(path)
530            .map(|bytes| ImageFile::new(bytes).map(Arc::new))
531    }
532    /// The font file at `path`, checked, or why it isn't one; `None` when
533    /// there is no such file. A store that keeps files between resolves
534    /// keeps them checked too.
535    fn font(&self, path: &str) -> Option<Result<Arc<FontFile>, String>> {
536        self.bytes(path)
537            .map(|bytes| FontFile::new(bytes).map(Arc::new))
538    }
539}
540
541/// The names of the files directly in `folder`, from every path a store
542/// holds — `None` when it holds none there.
543pub fn listed<'a>(paths: impl Iterator<Item = &'a String>, folder: &str) -> Option<Vec<String>> {
544    let prefix = format!("{folder}/");
545    let mut names: Vec<String> = paths
546        .filter_map(|p| p.strip_prefix(&prefix))
547        .filter(|rest| !rest.contains('/'))
548        .map(str::to_string)
549        .collect();
550    names.sort();
551    (!names.is_empty()).then_some(names)
552}
553
554impl Files for HashMap<String, String> {
555    fn bytes(&self, path: &str) -> Option<Vec<u8>> {
556        self.get(path).map(|s| s.clone().into_bytes())
557    }
558    fn list(&self, path: &str) -> Option<Vec<String>> {
559        listed(self.keys(), path)
560    }
561}
562
563/// A resolved design: its tree, each distinct image file its `Image`s show
564/// and font file its texts are set in, the files it was made from, and
565/// whether it is **themed** — whether any `theme.` reference reached its
566/// nodes.
567#[derive(Debug, Clone)]
568pub struct Scene {
569    pub root: Node,
570    pub images: ImageFiles,
571    pub fonts: FontFiles,
572    pub files: Vec<String>,
573    pub themed: bool,
574    /// Where each node is, by its id: what a patch finds its nodes by.
575    pub ids: Ids,
576}
577
578/// Two scenes are the same when they hold the same nodes, files and
579/// themes, however their children were placed.
580impl PartialEq for Scene {
581    fn eq(&self, other: &Scene) -> bool {
582        self.root == other.root
583            && self.images == other.images
584            && self.fonts == other.fonts
585            && self.files == other.files
586            && self.themed == other.themed
587    }
588}
589
590impl Scene {
591    /// A scene of `root` and its files, its ids indexed.
592    pub fn new(
593        root: Node,
594        images: ImageFiles,
595        fonts: FontFiles,
596        files: Vec<String>,
597        themed: bool,
598    ) -> Scene {
599        let ids = Ids::of(&root);
600        Scene {
601            root,
602            images,
603            fonts,
604            files,
605            themed,
606            ids,
607        }
608    }
609
610    /// The node with id `id`, if the scene has it.
611    pub fn node(&self, id: &NodeId) -> Option<&Node> {
612        self.at(self.ids.get(id)?)
613    }
614
615    /// The node at `order`, if any.
616    pub fn at(&self, order: &[OrderKey]) -> Option<&Node> {
617        let mut at = &self.root;
618        for k in order {
619            at = at.children.get(k)?;
620        }
621        Some(at)
622    }
623
624    /// The node at `order`, to change, if any.
625    pub fn at_mut(&mut self, order: &[OrderKey]) -> Option<&mut Node> {
626        let mut at = &mut self.root;
627        for k in order {
628            at = at.children.get_mut(k)?;
629        }
630        Some(at)
631    }
632}
633
634impl crate::tree::Node for Node {
635    fn id(&self) -> &NodeId {
636        &self.id
637    }
638    fn children(&self) -> &Children<Node> {
639        &self.children
640    }
641}
642
643/// A scene's image files by id, each decoded once however many `Image`s
644/// show it.
645pub type ImageFiles = std::collections::BTreeMap<ImageId, Arc<ImageFile>>;
646
647/// Image files compare by their identity, which is their bytes.
648impl PartialEq for ImageFile {
649    fn eq(&self, other: &ImageFile) -> bool {
650        self.id == other.id
651    }
652}
653
654/// Resolves the design at `path`, reporting every problem found.
655pub fn resolve(files: &dyn Files, path: &str) -> Result<Scene, Vec<Problem>> {
656    resolved(files, path).map(|(scene, _)| scene)
657}
658
659/// The design at `path` resolved, with the record of where each node came
660/// from and what it read — what resolving it again starts from.
661fn resolved(files: &dyn Files, path: &str) -> Result<(Scene, Record), Vec<Problem>> {
662    let mut r = Resolver::new(files);
663    let root = r.design(path, &[], None, "", None);
664    if r.problems.is_empty() {
665        let scene = Scene::new(
666            root.expect("no problems means a root"),
667            r.images,
668            r.fonts,
669            r.read,
670            r.themed,
671        );
672        Ok((scene, r.record))
673    } else {
674        Err(r.problems)
675    }
676}
677
678impl<'a> Resolver<'a> {
679    fn new(files: &'a dyn Files) -> Resolver<'a> {
680        Resolver {
681            files,
682            problems: Vec::new(),
683            read: Vec::new(),
684            stack: Vec::new(),
685            missing: Vec::new(),
686            depth: 0,
687            images: ImageFiles::new(),
688            by_path: HashMap::new(),
689            fonts: FontFiles::new(),
690            families: HashMap::new(),
691            themes: HashMap::new(),
692            themed: false,
693            instance: Vec::new(),
694            record: Record::default(),
695            current: None,
696            own_only: false,
697        }
698    }
699}
700
701struct Resolver<'a> {
702    files: &'a dyn Files,
703    problems: Vec<Problem>,
704    read: Vec<String>,
705    stack: Vec<String>,
706    /// Imports already reported as `no such file`, so a use of one isn't
707    /// reported a second time.
708    missing: Vec<String>,
709    /// How many elements enclose the current one, across instances.
710    depth: usize,
711    /// Every image file the design shows, by id.
712    images: ImageFiles,
713    /// Each image file read so far, by its path.
714    by_path: HashMap<String, Result<ImageId, String>>,
715    /// Every font file the design's texts are set in, by id.
716    fonts: FontFiles,
717    /// Each font a `themes.yml` named and a text used so far, file or
718    /// folder, by its path, and whether its problem was reported yet: once
719    /// per resolve, however many texts use it.
720    families: HashMap<String, (Found, bool)>,
721    /// The themes each folder's designs use, merged from every `themes.yml`
722    /// from that folder up, by folder: read once per resolve.
723    themes: HashMap<String, Rc<Themes>>,
724    /// Whether a `theme.` reference reached a node.
725    themed: bool,
726    /// The element ids of the instances enclosing the element being
727    /// resolved, outermost first: the start of every id made here.
728    instance: Vec<u32>,
729    /// Where each node made came from and what it read.
730    record: Record,
731    /// The node whose attributes are being resolved: what each file read
732    /// now is read by.
733    current: Option<NodeId>,
734    /// Whether to resolve an element's own attributes only, leaving its
735    /// child elements alone: resolving one node again.
736    own_only: bool,
737}
738
739/// Where a resolved design's nodes came from and what they read: what
740/// [`Resolution`] resolves again from when a file changes.
741#[derive(Debug, Clone, Default)]
742struct Record {
743    /// Each instance, by the element ids leading to it — the root design's
744    /// is empty.
745    instances: BTreeMap<Vec<u32>, Instance>,
746    /// The instances of each design file.
747    by_file: HashMap<String, BTreeSet<Vec<u32>>>,
748    /// The nodes and instances that read each file — or folder, by its path
749    /// and a `/` — by id: a change to it resolves them again.
750    readers: HashMap<String, BTreeSet<Vec<u32>>>,
751    /// What each node or instance reads, so it can be forgotten.
752    reads: BTreeMap<Vec<u32>, BTreeSet<String>>,
753    /// The nodes a `theme.` reference reached.
754    themed: BTreeSet<NodeId>,
755    /// The parse each design file was resolved from.
756    parses: HashMap<String, Arc<parse::File>>,
757}
758
759impl Record {
760    /// `id`, a node's or an instance's, reads `key`.
761    fn read_by(&mut self, key: &str, id: &[u32]) {
762        self.readers
763            .entry(key.to_string())
764            .or_default()
765            .insert(id.to_vec());
766        self.reads
767            .entry(id.to_vec())
768            .or_default()
769            .insert(key.to_string());
770    }
771
772    /// `id` reads nothing any more.
773    fn forget(&mut self, id: &[u32]) {
774        if let Some(keys) = self.reads.remove(id) {
775            for key in keys {
776                if let Some(readers) = self.readers.get_mut(&key) {
777                    readers.remove(id);
778                    if readers.is_empty() {
779                        self.readers.remove(&key);
780                    }
781                }
782            }
783        }
784    }
785}
786
787/// One use of a design file: the props its elements are resolved in, its
788/// imports, the layout of the frame it sits in, the caller's attributes laid
789/// over its root, and its root node's id.
790#[derive(Debug, Clone)]
791struct Instance {
792    file: String,
793    props: Arc<Props>,
794    imports: Arc<HashMap<String, String>>,
795    parent: Option<Layout>,
796    caller: Option<Caller>,
797    root: NodeId,
798}
799
800/// The file an instance is used in, and the attributes it lays over the
801/// instance's root — `x`, `y`, `width` and the like — as given there.
802#[derive(Debug, Clone)]
803struct Caller {
804    file: String,
805    overrides: Vec<(parse::Attr, String)>,
806}
807
808/// Whether a value as written names a theme color.
809fn writes_theme(v: &Value) -> bool {
810    match v {
811        Value::Theme(..) => true,
812        Value::Str(s, _) => s.starts_with("linear-gradient(") && s.contains("theme."),
813        Value::Array(items) => items.iter().any(writes_theme),
814        Value::Object(fields) => fields.iter().any(|(_, v)| writes_theme(v)),
815        _ => false,
816    }
817}
818
819/// Every `themes.yml` a design file at `path` reads its themes from: its
820/// folder's and each folder's above it.
821fn themes_chain(path: &str) -> Vec<String> {
822    let mut out = Vec::new();
823    let mut folder = path.rsplit_once('/').map(|(f, _)| f);
824    while let Some(f) = folder {
825        out.push(format!("{f}/{THEMES_FILE}"));
826        folder = f.rsplit_once('/').map(|(f, _)| f);
827    }
828    out.push(THEMES_FILE.to_string());
829    out
830}
831
832/// The themes a folder's designs use: every `themes.yml` with `kind: themes`
833/// from the top of the served folder down to it, merged — a lower file's
834/// names over a higher one's, theme by theme.
835#[derive(Debug, Clone, Default)]
836struct Themes {
837    /// Whether any such file was found.
838    found: bool,
839    light: BTreeMap<String, Rgba>,
840    dark: BTreeMap<String, Rgba>,
841    /// The fonts the files name, by name, a lower file's over a higher one's.
842    fonts: BTreeMap<String, ThemesFont>,
843}
844
845/// A font a `themes.yml`'s `fonts` names: its path as written, its address —
846/// `None` when it leaves the served folder — and where it is written.
847#[derive(Debug, Clone)]
848struct ThemesFont {
849    written: String,
850    at: Option<String>,
851    origin: Origin,
852}
853
854/// One `themes.yml`'s two themes and the fonts it names, as far as they are
855/// valid.
856struct ThemesFile {
857    light: BTreeMap<String, Rgba>,
858    dark: BTreeMap<String, Rgba>,
859    fonts: BTreeMap<String, ThemesFont>,
860}
861
862/// The file a folder's themes are in.
863pub const THEMES_FILE: &str = "themes.yml";
864
865/// How deep a resolved design's tree may nest, counting through instances:
866/// the parser's own limit for one file, and so far beyond any real design.
867const MAX_DEPTH: usize = parse::MAX_DEPTH;
868
869/// A prop's value and the file that wrote it, which a path in it is
870/// relative to.
871#[derive(Debug, Clone)]
872struct Given {
873    value: Value,
874    by: String,
875}
876
877type Props = HashMap<String, Given>;
878
879/// A font a `font` names: its family, or why it can't be used; `None` when
880/// there is nothing there.
881type Found = Option<Result<Rc<Family>, String>>;
882
883impl Resolver<'_> {
884    /// The node being resolved reads the file `key` — a folder's listing
885    /// when it ends in `/`: a change to it resolves the node again.
886    fn read_now(&mut self, key: &str) {
887        if let Some(node) = &self.current {
888            let node = node.0.clone();
889            self.record.read_by(key, &node);
890        }
891    }
892
893    fn problem(&mut self, file: &str, pos: Pos, message: String) {
894        self.problems.push(Problem {
895            file: file.to_string(),
896            pos,
897            message,
898        });
899    }
900
901    /// Binds every theme color written in `el` and the elements in it — in
902    /// `path`'s file — to that file's themes.
903    fn bind_element(&mut self, path: &str, el: &mut Element) {
904        self.bind_own(path, el);
905        for child in &mut el.children {
906            if let Child::Element(e) = child {
907                self.bind_element(path, e);
908            }
909        }
910    }
911
912    /// Binds the theme colors written in `el`'s own attributes, the node or
913    /// instance it makes reading `path`'s themes when there are any.
914    fn bind_own(&mut self, path: &str, el: &mut Element) {
915        let themed = el.attrs.iter().any(|a| writes_theme(&a.value));
916        for attr in &mut el.attrs {
917            self.bind(path, &mut attr.value);
918        }
919        if themed {
920            let mut id = self.instance.clone();
921            id.push(el.id);
922            for file in themes_chain(path) {
923                self.record.read_by(&file, &id);
924            }
925        }
926    }
927
928    /// Binds a value written in `path` to `path`'s themes: `theme.name` to
929    /// its two colors, and a gradient naming theme colors to one gradient
930    /// per theme.
931    fn bind(&mut self, path: &str, value: &mut Value) {
932        match value {
933            Value::Theme(name, pos) => {
934                // A problem already when there is none; any color keeps
935                // the rest of the design checked.
936                let (light, dark) = self
937                    .theme_color(path, name, *pos)
938                    .map_or(("#0000".into(), "#0000".into()), |(l, d)| (hex(l), hex(d)));
939                *value = Value::Themed {
940                    light,
941                    dark,
942                    written: format!("theme.{name}"),
943                };
944            }
945            Value::Str(s, pos) if s.starts_with("linear-gradient(") && s.contains("theme.") => {
946                let (mut light, mut dark) = (String::new(), String::new());
947                let mut rest = s.as_str();
948                let mut col = pos.col + 1;
949                while let Some(at) = rest.find("theme.") {
950                    let before = &rest[..at];
951                    light.push_str(before);
952                    dark.push_str(before);
953                    col += before.chars().count() as u32;
954                    let after = &rest[at + "theme.".len()..];
955                    let len = after
956                        .find(|c: char| !(c.is_alphanumeric() || c == '_' || c == '-'))
957                        .unwrap_or(after.len());
958                    let name = &after[..len];
959                    let at_ref = Pos {
960                        line: pos.line,
961                        col,
962                    };
963                    match self.theme_color(path, name, at_ref) {
964                        Some((l, d)) => {
965                            light.push_str(&hex(l));
966                            dark.push_str(&hex(d));
967                        }
968                        // A problem already; any color keeps the rest checked.
969                        None => {
970                            light.push_str("#0000");
971                            dark.push_str("#0000");
972                        }
973                    }
974                    col += ("theme.".len() + name.chars().count()) as u32;
975                    rest = &after[len..];
976                }
977                light.push_str(rest);
978                dark.push_str(rest);
979                *value = Value::Themed {
980                    light,
981                    dark,
982                    written: format!("\"{s}\""),
983                };
984            }
985            Value::Array(items) => {
986                for item in items {
987                    self.bind(path, item);
988                }
989            }
990            Value::Object(fields) => {
991                for (_, v) in fields {
992                    self.bind(path, v);
993                }
994            }
995            _ => {}
996        }
997    }
998
999    /// The two colors of `theme.name`, written at `pos` in `path`, from the
1000    /// themes of `path`'s folder — or `None`, with the problem reported.
1001    fn theme_color(&mut self, path: &str, name: &str, pos: Pos) -> Option<(Rgba, Rgba)> {
1002        let folder = path.rsplit_once('/').map_or("", |(folder, _)| folder);
1003        let themes = self.themes(folder);
1004        let problem = if !themes.found {
1005            format!(
1006                "theme.{name}: no {THEMES_FILE} with kind: themes in this design's folder or above it"
1007            )
1008        } else {
1009            match (themes.light.get(name), themes.dark.get(name)) {
1010                (Some(l), Some(d)) => return Some((*l, *d)),
1011                (None, None) => {
1012                    let mut names: Vec<&String> =
1013                        themes.light.keys().chain(themes.dark.keys()).collect();
1014                    names.sort();
1015                    names.dedup();
1016                    let names: Vec<&str> = names.iter().map(|n| n.as_str()).collect();
1017                    if names.is_empty() {
1018                        format!("theme.{name}: the themes have no colors")
1019                    } else {
1020                        format!(
1021                            "theme.{name}: the themes have no {name}; they have {}",
1022                            names.join(", ")
1023                        )
1024                    }
1025                }
1026                (Some(_), None) => format!(
1027                    "theme.{name}: only the light theme has {name}; a name needs a color in both"
1028                ),
1029                (None, Some(_)) => format!(
1030                    "theme.{name}: only the dark theme has {name}; a name needs a color in both"
1031                ),
1032            }
1033        };
1034        self.problem(path, pos, problem);
1035        None
1036    }
1037
1038    /// The themes the designs in `folder` use: its own `themes.yml` over
1039    /// those of the folders above it, each read once per resolve — a missing
1040    /// one too, so that creating it counts as a change.
1041    fn themes(&mut self, folder: &str) -> Rc<Themes> {
1042        if let Some(themes) = self.themes.get(folder) {
1043            return themes.clone();
1044        }
1045        let above = match folder.rsplit_once('/') {
1046            Some((parent, _)) => self.themes(parent),
1047            None if folder.is_empty() => Rc::new(Themes::default()),
1048            None => self.themes(""),
1049        };
1050        let file = if folder.is_empty() {
1051            THEMES_FILE.to_string()
1052        } else {
1053            format!("{folder}/{THEMES_FILE}")
1054        };
1055        let themes = match self.themes_file(&file) {
1056            None => above,
1057            Some(own) => {
1058                let mut merged = (*above).clone();
1059                merged.found = true;
1060                merged.light.extend(own.light);
1061                merged.dark.extend(own.dark);
1062                merged.fonts.extend(own.fonts);
1063                Rc::new(merged)
1064            }
1065        };
1066        self.themes.insert(folder.to_string(), themes.clone());
1067        themes
1068    }
1069
1070    /// The themes in the file at `path`, as far as they are valid, with its
1071    /// problems reported; `None` when there is no such file or it isn't a
1072    /// themes file — its `kind` isn't `themes`.
1073    fn themes_file(&mut self, path: &str) -> Option<ThemesFile> {
1074        let text = self.files.read(path)?;
1075        if !self.read.iter().any(|p| p == path) {
1076            self.read.push(path.to_string());
1077        }
1078        let mut file = ThemesFile {
1079            light: BTreeMap::new(),
1080            dark: BTreeMap::new(),
1081            fonts: BTreeMap::new(),
1082        };
1083        let yaml: serde_yaml::Value = match serde_yaml::from_str(&text) {
1084            Ok(yaml) => yaml,
1085            Err(e) => {
1086                // Only a file that says it is a themes file is ours to report.
1087                if !text.lines().any(|l| l.trim() == "kind: themes") {
1088                    return None;
1089                }
1090                let pos = e.location().map_or(Pos { line: 1, col: 1 }, |l| Pos {
1091                    line: l.line() as u32,
1092                    col: l.column() as u32,
1093                });
1094                self.problem(path, pos, format!("this themes file isn't valid YAML: {e}"));
1095                return Some(file);
1096            }
1097        };
1098        let serde_yaml::Value::Mapping(top) = yaml else {
1099            return None;
1100        };
1101        if top.get("kind").and_then(|k| k.as_str()) != Some("themes") {
1102            return None;
1103        }
1104        let start = Pos { line: 1, col: 1 };
1105        for (key, value) in &top {
1106            let key = key.as_str().unwrap_or_default();
1107            let theme = match key {
1108                "kind" => continue,
1109                "light" => &mut file.light,
1110                "dark" => &mut file.dark,
1111                "fonts" => {
1112                    self.themes_fonts(path, &text, value, &mut file.fonts);
1113                    continue;
1114                }
1115                other => {
1116                    let pos = key_pos(&text, None, other).unwrap_or(start);
1117                    self.problem(
1118                        path,
1119                        pos,
1120                        format!("{other}: a themes file has kind, fonts, light and dark only"),
1121                    );
1122                    continue;
1123                }
1124            };
1125            let at = key_pos(&text, None, key).unwrap_or(start);
1126            let serde_yaml::Value::Mapping(colors) = value else {
1127                self.problem(
1128                    path,
1129                    at,
1130                    format!("{key}: expected a map of names to colors, like surface: \"#ffffff\""),
1131                );
1132                continue;
1133            };
1134            for (name, color) in colors {
1135                let Some(name) = name.as_str() else {
1136                    self.problem(path, at, format!("{key}: a color's name is text"));
1137                    continue;
1138                };
1139                let pos = key_pos(&text, Some(key), name).unwrap_or(at);
1140                if name.is_empty()
1141                    || name.starts_with(|c: char| c.is_ascii_digit() || c == '-')
1142                    || !name
1143                        .chars()
1144                        .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
1145                {
1146                    self.problem(
1147                        path,
1148                        pos,
1149                        format!(
1150                            "{key}.{name}: a name holds letters, digits, _ and -, and starts with a letter"
1151                        ),
1152                    );
1153                    continue;
1154                }
1155                let value = match color {
1156                    serde_yaml::Value::String(s) => rgba(s),
1157                    serde_yaml::Value::Null => Err(
1158                        "no color: write it in quotes, \"#rrggbb\", as YAML reads # as a comment"
1159                            .to_string(),
1160                    ),
1161                    _ => Err("expected a color like \"#rrggbb\"".to_string()),
1162                };
1163                match value {
1164                    Ok(c) => {
1165                        theme.insert(name.to_string(), c);
1166                    }
1167                    Err(e) => self.problem(path, pos, format!("{key}.{name}: {e}")),
1168                }
1169            }
1170        }
1171        for needed in ["light", "dark"] {
1172            if !top.contains_key(needed) {
1173                self.problem(
1174                    path,
1175                    start,
1176                    format!("a themes file has a light and a dark theme; this one has no {needed}"),
1177                );
1178            }
1179        }
1180        Some(file)
1181    }
1182
1183    /// The fonts a `themes.yml` at `path` names in `value`, its `fonts`, each
1184    /// a name and a path from that file; a bad entry is a problem at its
1185    /// line.
1186    fn themes_fonts(
1187        &mut self,
1188        path: &str,
1189        text: &str,
1190        value: &serde_yaml::Value,
1191        into: &mut BTreeMap<String, ThemesFont>,
1192    ) {
1193        let start = Pos { line: 1, col: 1 };
1194        let at = key_pos(text, None, "fonts").unwrap_or(start);
1195        let serde_yaml::Value::Mapping(entries) = value else {
1196            self.problem(
1197                path,
1198                at,
1199                "fonts: expected a map of names to font files or folders, like figtree: ./fonts/Figtree".into(),
1200            );
1201            return;
1202        };
1203        for (name, written) in entries {
1204            let Some(name) = name.as_str() else {
1205                self.problem(path, at, "fonts: a font's name is text".into());
1206                continue;
1207            };
1208            let pos = key_pos(text, Some("fonts"), name).unwrap_or(at);
1209            if name.is_empty()
1210                || !name
1211                    .chars()
1212                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
1213            {
1214                self.problem(
1215                    path,
1216                    pos,
1217                    format!(
1218                        "fonts.{name}: a font's name holds lowercase letters, digits and - only"
1219                    ),
1220                );
1221                continue;
1222            }
1223            let Some(written) = written.as_str() else {
1224                self.problem(
1225                    path,
1226                    pos,
1227                    format!("fonts.{name}: expected a path to a font file or a folder of one family's, like ./fonts/Figtree"),
1228                );
1229                continue;
1230            };
1231            into.insert(
1232                name.to_string(),
1233                ThemesFont {
1234                    written: written.to_string(),
1235                    at: join(path, written),
1236                    origin: Origin {
1237                        file: path.to_string(),
1238                        pos,
1239                    },
1240                },
1241            );
1242        }
1243    }
1244
1245    /// A design file's imports, each to the file it names, and its root's
1246    /// declared props — with the problems of either reported.
1247    /// `None` when its root isn't a `Frame`.
1248    fn scope(
1249        &mut self,
1250        path: &str,
1251        file: &parse::File,
1252    ) -> Option<(HashMap<String, String>, Props)> {
1253        let mut imports = HashMap::new();
1254        for import in &file.imports {
1255            if import.name == parse::THEME {
1256                self.problem(
1257                    path,
1258                    import.pos,
1259                    format!(
1260                        "import {0} — {0} is the name of the design's themes",
1261                        import.name
1262                    ),
1263                );
1264                continue;
1265            }
1266            if ELEMENTS.contains(&import.name.as_str()) {
1267                self.problem(
1268                    path,
1269                    import.pos,
1270                    format!("import {0} — {0} is a built-in element", import.name),
1271                );
1272                continue;
1273            }
1274            if imports.contains_key(&import.name) {
1275                self.problem(
1276                    path,
1277                    import.pos,
1278                    format!("import {0} — {0} is already imported", import.name),
1279                );
1280                continue;
1281            }
1282            if !import.path.ends_with(".design") {
1283                self.problem(
1284                    path,
1285                    import.pos,
1286                    format!("import {} — a design is a .design file", import.path),
1287                );
1288                continue;
1289            }
1290            match join(path, &import.path) {
1291                Some(target) => {
1292                    // Every import must exist, used or not.
1293                    if self.files.read(&target).is_none() {
1294                        self.problem(
1295                            path,
1296                            import.pos,
1297                            format!("import {} — no such file", import.path),
1298                        );
1299                        self.missing.push(target.clone());
1300                    }
1301                    imports.insert(import.name.clone(), target);
1302                }
1303                None => self.problem(
1304                    path,
1305                    import.pos,
1306                    format!("import {} — outside the served folder", import.path),
1307                ),
1308            }
1309        }
1310        if file.root.name != "Frame" {
1311            self.problem(
1312                path,
1313                file.root.pos,
1314                format!("the root element must be a Frame, not {}", file.root.name),
1315            );
1316            return None;
1317        }
1318        // Props: the root's declared defaults, overridden by the instance's.
1319        let mut props = Props::new();
1320        if let Some(attr) = file.root.attrs.iter().find(|a| a.name == "props") {
1321            match &attr.value {
1322                Value::Object(fields) => {
1323                    for (k, v) in fields {
1324                        if k == parse::THEME {
1325                            self.problem(
1326                                path,
1327                                attr.pos,
1328                                format!(
1329                                    "props: {k} is the name of the design's themes, not a prop"
1330                                ),
1331                            );
1332                            continue;
1333                        }
1334                        props.insert(
1335                            k.clone(),
1336                            Given {
1337                                value: v.clone(),
1338                                by: path.to_string(),
1339                            },
1340                        );
1341                    }
1342                }
1343                _ => self.problem(
1344                    path,
1345                    attr.pos,
1346                    "props: expected an object of defaults".into(),
1347                ),
1348            }
1349        }
1350        Some((imports, props))
1351    }
1352
1353    /// The root node of the design at `path`, with `args` given to its props.
1354    fn design(
1355        &mut self,
1356        path: &str,
1357        args: &[(parse::Attr, String)],
1358        from: Option<(&str, Pos)>,
1359        used_as: &str,
1360        parent: Option<Layout>,
1361    ) -> Option<Node> {
1362        if self.stack.iter().any(|p| p == path) {
1363            let (file, pos) = from.unwrap_or((path, Pos { line: 1, col: 1 }));
1364            self.problem(file, pos, format!("{path} uses itself"));
1365            return None;
1366        }
1367        let Some(source) = self.files.read(path) else {
1368            if from.is_some() && self.missing.iter().any(|p| p == path) {
1369                return None;
1370            }
1371            let (file, pos) = from.unwrap_or((path, Pos { line: 1, col: 1 }));
1372            self.problem(file, pos, format!("{path} — no such file"));
1373            return None;
1374        };
1375        if !self.read.iter().any(|p| p == path) {
1376            self.read.push(path.to_string());
1377        }
1378        let mut file = match self.files.parse(path, &source) {
1379            Ok(file) => {
1380                self.record.parses.insert(path.to_string(), file.clone());
1381                (*file).clone()
1382            }
1383            Err(mut problem) => {
1384                problem.file = path.to_string();
1385                self.problems.push(problem);
1386                return None;
1387            }
1388        };
1389        // Bound now, before any value leaves this file as a prop: a theme
1390        // color is this file's, wherever it is used.
1391        self.bind_element(path, &mut file.root);
1392        let (imports, mut props) = self.scope(path, &file)?;
1393        let (caller, caller_pos) = from.unwrap_or((path, file.root.pos));
1394        let caller = caller.to_string();
1395        let mut overrides = Vec::new();
1396        for (arg, by) in args {
1397            if matches!(
1398                arg.name.as_str(),
1399                "x" | "y" | "width" | "height" | "name" | "opacity" | "onClick"
1400            ) {
1401                overrides.push((arg.clone(), by.clone()));
1402            } else if props.contains_key(&arg.name) {
1403                props.insert(
1404                    arg.name.clone(),
1405                    Given {
1406                        value: arg.value.clone(),
1407                        by: by.clone(),
1408                    },
1409                );
1410            } else {
1411                self.problem(
1412                    &caller,
1413                    arg.pos,
1414                    format!("{used_as} has no prop \"{}\"", arg.name),
1415                );
1416            }
1417        }
1418        let _ = caller_pos;
1419        let key = self.instance.clone();
1420        for target in imports.values() {
1421            self.record.read_by(target, &key);
1422        }
1423        self.record.instances.insert(
1424            key.clone(),
1425            Instance {
1426                file: path.to_string(),
1427                props: Arc::new(props.clone()),
1428                imports: Arc::new(imports.clone()),
1429                parent,
1430                caller: from.map(|_| Caller {
1431                    file: caller.clone(),
1432                    overrides: overrides.clone(),
1433                }),
1434                root: {
1435                    let mut id = key.clone();
1436                    id.push(file.root.id);
1437                    NodeId(id)
1438                },
1439            },
1440        );
1441        self.record
1442            .by_file
1443            .entry(path.to_string())
1444            .or_default()
1445            .insert(key);
1446        self.stack.push(path.to_string());
1447        let mut root = self.element(path, &file.root, &props, &imports, true, parent);
1448        self.stack.pop();
1449        if let Some(root) = root.as_mut() {
1450            self.current = Some(root.id.clone());
1451            for (attr, by) in &overrides {
1452                // The caller's own props are already substituted into these.
1453                self.common(&caller, by, root, attr, attr.value.clone());
1454                self.fill_needs_layout(&caller, attr, &attr.value, parent);
1455                self.place_needs_no_layout(&caller, attr, parent);
1456            }
1457        }
1458        root
1459    }
1460
1461    fn element(
1462        &mut self,
1463        path: &str,
1464        el: &Element,
1465        props: &Props,
1466        imports: &HashMap<String, String>,
1467        root: bool,
1468        parent: Option<Layout>,
1469    ) -> Option<Node> {
1470        if self.depth >= MAX_DEPTH {
1471            self.problem(path, el.pos, format!("nested more than {MAX_DEPTH} deep"));
1472            return None;
1473        }
1474        self.depth += 1;
1475        let node = self.element_inner(path, el, props, imports, root, parent);
1476        self.depth -= 1;
1477        node
1478    }
1479
1480    fn element_inner(
1481        &mut self,
1482        path: &str,
1483        el: &Element,
1484        props: &Props,
1485        imports: &HashMap<String, String>,
1486        root: bool,
1487        parent: Option<Layout>,
1488    ) -> Option<Node> {
1489        // An attribute given twice is a problem, on an element or an instance
1490        // alike; the first one counts.
1491        let mut seen: Vec<&str> = Vec::new();
1492        let mut attrs = Vec::new();
1493        for attr in &el.attrs {
1494            if seen.contains(&attr.name.as_str()) {
1495                self.problem(path, attr.pos, format!("{} is given twice", attr.name));
1496            } else {
1497                seen.push(&attr.name);
1498                attrs.push(attr);
1499            }
1500        }
1501        if let Some(target) = imports.get(&el.name) {
1502            if !el.children.is_empty() {
1503                self.problem(path, el.pos, format!("{} takes no children", el.name));
1504            }
1505            let args: Vec<(parse::Attr, String)> = attrs
1506                .iter()
1507                .filter(|a| a.name != "key" || !self.key_ok(path, a))
1508                .map(|a| {
1509                    let (value, by) = substitute(&a.value, props, path);
1510                    let arg = parse::Attr {
1511                        name: a.name.clone(),
1512                        value,
1513                        pos: a.pos,
1514                    };
1515                    (arg, by)
1516                })
1517                .collect();
1518            let target = target.clone();
1519            self.instance.push(el.id);
1520            let node = self.design(&target, &args, Some((path, el.pos)), &el.name, parent);
1521            self.instance.pop();
1522            return node;
1523        }
1524        let kind = match el.name.as_str() {
1525            // Keep in step with ELEMENTS.
1526            "Frame" => Kind::Frame,
1527            "Text" => Kind::Text,
1528            "Rect" => Kind::Rect,
1529            "Ellipse" => Kind::Ellipse,
1530            "Vector" => Kind::Vector,
1531            "Image" => Kind::Image,
1532            other => {
1533                self.problem(path, el.pos, format!("unknown element {other}"));
1534                return None;
1535            }
1536        };
1537        let mut node = Node::new(kind, &el.name);
1538        let mut id = self.instance.clone();
1539        id.push(el.id);
1540        node.id = NodeId(id);
1541        self.current = Some(node.id.clone());
1542        // A text's `font`, as given and by the file that wrote it, chosen a
1543        // face from once its weight is known.
1544        let mut font = None;
1545        for &attr in &attrs {
1546            if attr.name == "props" {
1547                if !root {
1548                    self.problem(
1549                        path,
1550                        attr.pos,
1551                        "props: only a design's root declares props".into(),
1552                    );
1553                }
1554                continue;
1555            }
1556            // Shown on its own a design has no parent to place it in, and used
1557            // as an instance it goes where the instance's own x and y say.
1558            if root && matches!(attr.name.as_str(), "x" | "y") {
1559                self.problem(
1560                    path,
1561                    attr.pos,
1562                    format!(
1563                        "{}: a design's root has no place of its own; place an instance with x and y where it is used",
1564                        attr.name
1565                    ),
1566                );
1567                continue;
1568            }
1569            let (value, by) = match &attr.value {
1570                Value::Prop(p) => match props.get(p) {
1571                    Some(given) => (given.value.clone(), given.by.clone()),
1572                    None => {
1573                        self.problem(path, attr.pos, format!("{}: no prop \"{p}\"", attr.name));
1574                        continue;
1575                    }
1576                },
1577                v => (v.clone(), path.to_string()),
1578            };
1579            if has_theme(&value) {
1580                self.themed = true;
1581                self.record.themed.insert(node.id.clone());
1582            }
1583            self.fill_needs_layout(path, attr, &value, parent);
1584            self.place_needs_no_layout(path, attr, parent);
1585            if kind == Kind::Text && attr.name == "font" {
1586                font = Some((attr, value, by));
1587                continue;
1588            }
1589            self.attribute(path, &by, &mut node, attr, value);
1590        }
1591        match kind {
1592            Kind::Vector if !el.attrs.iter().any(|a| a.name == "d") => self.problem(
1593                path,
1594                el.pos,
1595                format!("{} needs a path, d=\"M…\"", node.name),
1596            ),
1597            Kind::Image if node.image.is_none() && !el.attrs.iter().any(|a| a.name == "src") => {
1598                self.problem(
1599                    path,
1600                    el.pos,
1601                    format!("{} needs a file, src=\"…\"", node.name),
1602                )
1603            }
1604            _ => {}
1605        }
1606        if kind == Kind::Text {
1607            let mut content = String::new();
1608            for child in &el.children {
1609                match child {
1610                    Child::Text(parts, pos) => {
1611                        for part in parts {
1612                            match part {
1613                                TextPart::Literal(s) => content.push_str(s),
1614                                TextPart::Prop(p) => match props.get(p).map(|g| &g.value) {
1615                                    Some(Value::Str(s, _)) => content.push_str(s),
1616                                    Some(Value::Num(n)) => content.push_str(&n.to_string()),
1617                                    _ => self.problem(path, *pos, format!("no text prop \"{p}\"")),
1618                                },
1619                                TextPart::Theme(name, at) => self.problem(
1620                                    path,
1621                                    *at,
1622                                    format!(
1623                                        "theme.{name} is a color, not text: give it to color={{theme.{name}}}"
1624                                    ),
1625                                ),
1626                            }
1627                        }
1628                    }
1629                    Child::Element(e) => self.problem(path, e.pos, "Text holds text only".into()),
1630                }
1631            }
1632            let text = node.text.get_or_insert_with(default_text);
1633            text.content = content;
1634            let weight = text.weight;
1635            let weight_at = attrs.iter().find(|a| a.name == "weight").map(|a| a.pos);
1636            text.font = self.text_font(path, el.pos, font, weight, weight_at);
1637        } else {
1638            let mut keys: HashMap<&str, Pos> = HashMap::new();
1639            for child in &el.children {
1640                if let Child::Element(e) = child
1641                    && let Some(k) = crate::ids::key(e)
1642                    && keys.insert(k, e.pos).is_some()
1643                {
1644                    let at = e
1645                        .attrs
1646                        .iter()
1647                        .find(|a| a.name == "key")
1648                        .map_or(e.pos, |a| a.pos);
1649                    self.problem(path, at, format!("key: {k:?} is another sibling's key too"));
1650                }
1651                match child {
1652                    Child::Element(_) if self.own_only => {}
1653                    Child::Element(e) => {
1654                        if kind != Kind::Frame {
1655                            self.problem(path, e.pos, format!("{} takes no children", el.name));
1656                        } else if let Some(n) =
1657                            self.element(path, e, props, imports, false, Some(node.layout))
1658                        {
1659                            node.children.push(n);
1660                            self.current = Some(node.id.clone());
1661                        }
1662                    }
1663                    Child::Text(_, pos) => self.problem(
1664                        path,
1665                        *pos,
1666                        format!("text belongs in a Text, not a {}", el.name),
1667                    ),
1668                }
1669            }
1670        }
1671        Some(node)
1672    }
1673
1674    /// `"fill"` takes the free space of a parent's auto layout, so a child of a
1675    /// frame with no layout can't have it.
1676    fn fill_needs_layout(
1677        &mut self,
1678        path: &str,
1679        attr: &parse::Attr,
1680        value: &Value,
1681        parent: Option<Layout>,
1682    ) {
1683        if parent == Some(Layout::None)
1684            && matches!(attr.name.as_str(), "width" | "height")
1685            && matches!(value, Value::Str(s, _) if s == "fill")
1686        {
1687            self.problem(
1688                path,
1689                attr.pos,
1690                format!(
1691                    "{}: \"fill\" needs a parent with layout=\"row\" or \"column\"",
1692                    attr.name
1693                ),
1694            );
1695        }
1696    }
1697
1698    /// `x` and `y` place a child inside a frame with no layout; auto layout
1699    /// places its children itself, so there they would mean nothing.
1700    fn place_needs_no_layout(&mut self, path: &str, attr: &parse::Attr, parent: Option<Layout>) {
1701        if matches!(parent, Some(Layout::Row | Layout::Column))
1702            && matches!(attr.name.as_str(), "x" | "y")
1703        {
1704            self.problem(
1705                path,
1706                attr.pos,
1707                format!(
1708                    "{}: places a child only in a frame with layout=\"none\"",
1709                    attr.name
1710                ),
1711            );
1712        }
1713    }
1714
1715    // Each arm sets a field and says whether the value fit: `Ok(field = v)`.
1716    #[allow(clippy::unit_arg)]
1717    fn attribute(
1718        &mut self,
1719        path: &str,
1720        by: &str,
1721        node: &mut Node,
1722        attr: &parse::Attr,
1723        value: Value,
1724    ) {
1725        let kind = node.kind;
1726        // A path is relative to the file that wrote it, and its problem is
1727        // where it was written.
1728        let (at_file, at_pos) = written_at(path, by, attr, &value);
1729        let ok = match (attr.name.as_str(), kind) {
1730            ("x" | "y" | "width" | "height" | "name" | "opacity" | "onClick", _) => {
1731                self.common(path, by, node, attr, value);
1732                return;
1733            }
1734            ("key", _) => {
1735                self.key_ok(path, attr);
1736                return;
1737            }
1738            ("fill", k) if k != Kind::Text => fill(&value).map(|f| node.fill = Some(f)),
1739            ("stroke", k) if k != Kind::Text => color(&value).map(|c| node.stroke = Some(c)),
1740            ("strokeWidth", k) if k != Kind::Text => px(&value).map(|n| node.stroke_width = n),
1741            ("radius", k) if k != Kind::Text => match &value {
1742                Value::Num(_) => px(&value).map(|n| node.radius = [n; 4]),
1743                Value::Array(a) if a.len() == 4 => {
1744                    pxs(a).map(|v| node.radius = [v[0], v[1], v[2], v[3]])
1745                }
1746                _ => Err("expected pixels or an array of 4".into()),
1747            },
1748            ("shadow", k) if k != Kind::Text => shadow(&value).map(|mut s| {
1749                s.at = Origin {
1750                    file: path.to_string(),
1751                    pos: attr.pos,
1752                };
1753                node.shadow = Some(s)
1754            }),
1755            // Only a frame has children to clip.
1756            ("clip", Kind::Frame) => match value {
1757                Value::Bool(b) => Ok(node.clip = b),
1758                _ => Err("expected true or false".into()),
1759            },
1760            ("layout", Kind::Frame) => match str_of(&value).as_deref() {
1761                Some("row") => Ok(node.layout = Layout::Row),
1762                Some("column") => Ok(node.layout = Layout::Column),
1763                Some("none") => Ok(node.layout = Layout::None),
1764                _ => Err("expected \"row\", \"column\" or \"none\"".into()),
1765            },
1766            ("gap", Kind::Frame) => px(&value).map(|n| node.gap = n),
1767            ("padding", Kind::Frame) => match &value {
1768                Value::Num(_) => px(&value).map(|n| node.padding = [n; 4]),
1769                Value::Array(a) if a.len() == 2 => {
1770                    pxs(a).map(|v| node.padding = [v[0], v[1], v[0], v[1]])
1771                }
1772                Value::Array(a) if a.len() == 4 => {
1773                    pxs(a).map(|v| node.padding = [v[0], v[1], v[2], v[3]])
1774                }
1775                other => Err(format!(
1776                    "expected pixels or an array of 2 or 4, found {}",
1777                    show(other)
1778                )),
1779            },
1780            ("align", Kind::Frame) => align(&value, false).map(|a| node.align = a),
1781            ("justify", Kind::Frame) => align(&value, true).map(|a| node.justify = a),
1782            ("size", Kind::Text) => {
1783                px(&value).map(|n| node.text.get_or_insert_with(default_text).size = n)
1784            }
1785            ("weight", Kind::Text) => match num(&value) {
1786                Ok(w) if fonts::WEIGHTS.iter().any(|&k| k as f32 == w) => {
1787                    Ok(node.text.get_or_insert_with(default_text).weight = w as u16)
1788                }
1789                _ => Err("expected 100 to 900 in steps of 100".into()),
1790            },
1791            ("color", Kind::Text) => {
1792                color(&value).map(|c| node.text.get_or_insert_with(default_text).color = c)
1793            }
1794            ("lineHeight", Kind::Text) => {
1795                px(&value).map(|n| node.text.get_or_insert_with(default_text).line_height = n)
1796            }
1797            ("textAlign", Kind::Text) => match str_of(&value).as_deref() {
1798                Some("left") => {
1799                    Ok(node.text.get_or_insert_with(default_text).align = TextAlign::Left)
1800                }
1801                Some("center") => {
1802                    Ok(node.text.get_or_insert_with(default_text).align = TextAlign::Center)
1803                }
1804                Some("right") => {
1805                    Ok(node.text.get_or_insert_with(default_text).align = TextAlign::Right)
1806                }
1807                _ => Err("expected \"left\", \"center\" or \"right\"".into()),
1808            },
1809            ("d", Kind::Vector) => match str_of(&value) {
1810                Some(d) => match path_numbers(&d).map_err(|e| e.to_string()).and_then(|()| {
1811                    kurbo::BezPath::from_svg(&d).map_err(|e| format!("not SVG path data: {e}"))
1812                }) {
1813                    Ok(_) => {
1814                        let view_box = node.vector.as_ref().map(|v| v.view_box);
1815                        let view_box = view_box.unwrap_or_else(|| bounds(&d));
1816                        Ok(node.vector = Some(Vector { d, view_box }))
1817                    }
1818                    Err(e) => Err(e),
1819                },
1820                None => Err("expected SVG path data".into()),
1821            },
1822            ("viewBox", Kind::Vector) => match str_of(&value).as_deref().map(view_box) {
1823                Some(Ok(b)) => {
1824                    let v = node.vector.get_or_insert_with(|| Vector {
1825                        d: String::new(),
1826                        view_box: b,
1827                    });
1828                    Ok(v.view_box = b)
1829                }
1830                Some(Err(e)) => Err(e),
1831                None => Err("expected \"minX minY width height\"".into()),
1832            },
1833            ("src", Kind::Image) => match str_of(&value) {
1834                Some(src) => match join(by, &src) {
1835                    None => {
1836                        let e = format!("src: {src} — outside the served folder");
1837                        self.problem(&at_file, at_pos, e);
1838                        return;
1839                    }
1840                    Some(at) => match self.image_file(&at) {
1841                        None => {
1842                            self.problem(&at_file, at_pos, format!("src: {src} — no such file"));
1843                            return;
1844                        }
1845                        Some(Err(e)) => {
1846                            self.problem(&at_file, at_pos, format!("src: {src} {e}"));
1847                            return;
1848                        }
1849                        Some(Ok(id)) => {
1850                            node.image = Some(Image {
1851                                src: at,
1852                                id,
1853                                at: Origin {
1854                                    file: path.to_string(),
1855                                    pos: attr.pos,
1856                                },
1857                            });
1858                            Ok(())
1859                        }
1860                    },
1861                },
1862                None => Err("expected a path".into()),
1863            },
1864            (name, _) => {
1865                self.problem(
1866                    path,
1867                    attr.pos,
1868                    format!("{} takes no attribute {name}", node.name),
1869                );
1870                return;
1871            }
1872        };
1873        if let Err(e) = ok {
1874            self.problem(path, attr.pos, format!("{}: {e}", attr.name));
1875        }
1876    }
1877
1878    /// The image file at `at`, by its content — read and decoded once per
1879    /// resolve however many `Image`s show it — or why it doesn't decode;
1880    /// `None` when there is no such file.
1881    fn image_file(&mut self, at: &str) -> Option<Result<ImageId, String>> {
1882        self.read_now(at);
1883        if let Some(known) = self.by_path.get(at) {
1884            return Some(known.clone());
1885        }
1886        let file = self.files.image(at)?;
1887        if !self.read.iter().any(|p| p == at) {
1888            self.read.push(at.to_string());
1889        }
1890        let id = file.map(|file| {
1891            let id = file.id;
1892            self.images.entry(id).or_insert(file);
1893            id
1894        });
1895        self.by_path.insert(at.to_string(), id.clone());
1896        Some(id)
1897    }
1898
1899    /// The font file a `Text` written at `pos` in `path` is set in, for its
1900    /// `weight` — `None` for Inter, when it has no `font`. Its `font`, given
1901    /// as `value` by the file `by`, is a name the themes of `by`'s folder
1902    /// give a font. An unknown name is a problem where it is written; a font
1903    /// that can't be used, at its entry in the `themes.yml`, once; a weight
1904    /// the font lacks, at the `weight` — or the `Text`.
1905    fn text_font(
1906        &mut self,
1907        path: &str,
1908        pos: Pos,
1909        font: Option<(&parse::Attr, Value, String)>,
1910        weight: u16,
1911        weight_at: Option<Pos>,
1912    ) -> Option<FontId> {
1913        let Some((attr, value, by)) = font else {
1914            if !fonts::INTER_WEIGHTS.contains(&weight) {
1915                self.no_weight(
1916                    path,
1917                    pos,
1918                    weight_at,
1919                    weight,
1920                    fonts::INTER,
1921                    &fonts::INTER_WEIGHTS,
1922                );
1923            }
1924            return None;
1925        };
1926        let (file, at_pos) = written_at(path, &by, attr, &value);
1927        let Some(name) = str_of(&value) else {
1928            self.problem(
1929                &file,
1930                at_pos,
1931                "font: expected the name of a font the themes name, like \"figtree\"".into(),
1932            );
1933            return None;
1934        };
1935        let folder = by.rsplit_once('/').map_or("", |(folder, _)| folder);
1936        for file in themes_chain(&by) {
1937            self.read_now(&file);
1938        }
1939        let named = self.themes(folder).fonts.clone();
1940        let Some(entry) = named.get(&name) else {
1941            let problem = if named.is_empty() {
1942                format!(
1943                    "font: no font named {name}; no {THEMES_FILE} in this design's folder or above it names fonts"
1944                )
1945            } else {
1946                let names: Vec<&str> = named.keys().map(String::as_str).collect();
1947                format!(
1948                    "font: no font named {name}; the themes name {}",
1949                    names.join(", ")
1950                )
1951            };
1952            self.problem(&file, at_pos, problem);
1953            return None;
1954        };
1955        let origin = entry.origin.clone();
1956        let Some(at) = entry.at.clone() else {
1957            let key = format!("{}\0{}", origin.file, name);
1958            if let std::collections::hash_map::Entry::Vacant(first) = self.families.entry(key) {
1959                first.insert((None, true));
1960                self.problem(
1961                    &origin.file,
1962                    origin.pos,
1963                    format!(
1964                        "fonts.{name}: {} — outside the served folder",
1965                        entry.written
1966                    ),
1967                );
1968            }
1969            return None;
1970        };
1971        self.read_now(&at);
1972        self.read_now(&format!("{at}/"));
1973        let (family, reported) = self.family(&at);
1974        let family = match family {
1975            Some(Ok(family)) => family,
1976            missing_or_bad => {
1977                if !reported {
1978                    let problem = match missing_or_bad {
1979                        Some(Err(e)) => format!("fonts.{name}: {} {e}", entry.written),
1980                        _ => format!("fonts.{name}: {} — no such file or folder", entry.written),
1981                    };
1982                    self.problem(&origin.file, origin.pos, problem);
1983                    if let Some(known) = self.families.get_mut(&at) {
1984                        known.1 = true;
1985                    }
1986                }
1987                return None;
1988            }
1989        };
1990        match family.face(weight) {
1991            Some(face) => {
1992                self.fonts.entry(face.id).or_insert_with(|| face.clone());
1993                Some(face.id)
1994            }
1995            None => {
1996                let has = family.weights();
1997                self.no_weight(path, pos, weight_at, weight, &family.name, &has);
1998                None
1999            }
2000        }
2001    }
2002
2003    /// The problem of a weight the font doesn't have: at the `Text`'s
2004    /// `weight`, or at the `Text` when it leaves the weight at 400.
2005    fn no_weight(
2006        &mut self,
2007        path: &str,
2008        pos: Pos,
2009        weight_at: Option<Pos>,
2010        weight: u16,
2011        family: &str,
2012        has: &[u16],
2013    ) {
2014        let message = if has.is_empty() {
2015            format!(
2016                "weight: {family} has no {weight}; it has no weight from 100 to 900 in steps of 100"
2017            )
2018        } else {
2019            format!(
2020                "weight: {family} has no {weight}; it has {}",
2021                fonts::list(has)
2022            )
2023        };
2024        self.problem(path, weight_at.unwrap_or(pos), message);
2025    }
2026
2027    /// The font at `at` — one file, or a folder of one family's files — read
2028    /// and checked once per resolve, or why it can't be used; `None` when
2029    /// there is nothing there. Also whether its problem was reported yet.
2030    fn family(&mut self, at: &str) -> (Found, bool) {
2031        if let Some(known) = self.families.get(at) {
2032            return known.clone();
2033        }
2034        let family = match self.font_file(at) {
2035            Some(Ok(file)) => Some(Ok(Rc::new(Family {
2036                name: file.family.clone(),
2037                faces: vec![file],
2038            }))),
2039            Some(Err(e)) => Some(Err(e)),
2040            None => self.files.list(at).map(|names| {
2041                let mut faces = Vec::new();
2042                for name in names.into_iter().filter(|n| fonts::is_font_name(n)) {
2043                    match self.font_file(&format!("{at}/{name}")) {
2044                        Some(Ok(file)) => faces.push((name, file)),
2045                        Some(Err(e)) => return Err(format!("holds {name}, which {e}")),
2046                        // Gone since the folder was listed.
2047                        None => {}
2048                    }
2049                }
2050                Family::of_folder(faces).map(Rc::new)
2051            }),
2052        };
2053        self.families
2054            .insert(at.to_string(), (family.clone(), false));
2055        (family, false)
2056    }
2057
2058    /// The font file at `at`, checked, or why it isn't one; `None` when there
2059    /// is no such file.
2060    fn font_file(&mut self, at: &str) -> Option<Result<Arc<FontFile>, String>> {
2061        let file = self.files.font(at)?;
2062        if !self.read.iter().any(|p| p == at) {
2063            self.read.push(at.to_string());
2064        }
2065        Some(file)
2066    }
2067
2068    /// An interaction with its path, if it has one, made relative to the top of
2069    /// the served folder instead of to `path`, the file it is written in.
2070    fn interaction(&mut self, path: &str, action: &str) -> Result<String, String> {
2071        let Some((verb, target)) = action.split_once(' ') else {
2072            return Ok(action.to_string());
2073        };
2074        let target = target.trim();
2075        if !target.ends_with(".design") {
2076            return Err(format!("{action} — a design is a .design file"));
2077        }
2078        match join(path, target) {
2079            None => Err(format!("{action} — outside the served folder")),
2080            Some(at)
2081                if {
2082                    self.read_now(&at);
2083                    self.files.bytes(&at).is_none()
2084                } =>
2085            {
2086                Err(format!("{action} — no such file"))
2087            }
2088            Some(at) => Ok(format!("{verb} {at}")),
2089        }
2090    }
2091
2092    /// Whether `attr`, a `key`, is a string — the one value a parse can
2093    /// match siblings by — saying so where it isn't. True either way once
2094    /// it has been looked at, so an instance doesn't pass it on as a prop.
2095    fn key_ok(&mut self, path: &str, attr: &parse::Attr) -> bool {
2096        if !matches!(attr.value, Value::Str(..)) {
2097            self.problem(path, attr.pos, "key: expected a string".into());
2098        }
2099        true
2100    }
2101
2102    fn common(&mut self, path: &str, by: &str, node: &mut Node, attr: &parse::Attr, value: Value) {
2103        let (at_file, at_pos) = written_at(path, by, attr, &value);
2104        let ok = match attr.name.as_str() {
2105            "x" => num(&value).map(|n| node.x = n),
2106            "y" => num(&value).map(|n| node.y = n),
2107            "width" => size(&value).map(|s| node.width = s),
2108            "height" => size(&value).map(|s| node.height = s),
2109            "name" => str_of(&value)
2110                .map(|s| node.name = s)
2111                .ok_or_else(|| "expected a string".into()),
2112            "opacity" => match value {
2113                Value::Num(n) if (0.0..=1.0).contains(&n) => {
2114                    node.opacity = n as f32;
2115                    Ok(())
2116                }
2117                other => Err(format!(
2118                    "expected a number from 0 to 1, found {}",
2119                    show(&other)
2120                )),
2121            },
2122            "onClick" => match str_of(&value).filter(|s| interaction_ok(s)) {
2123                Some(action) => match self.interaction(by, &action) {
2124                    Ok(a) => {
2125                        node.on_click = Some(a);
2126                        Ok(())
2127                    }
2128                    Err(e) => {
2129                        self.problem(&at_file, at_pos, format!("onClick: {e}"));
2130                        return;
2131                    }
2132                },
2133                None => Err(
2134                    "expected \"navigate <path>\", \"back\", \"overlay <path>\" or \"close\""
2135                        .into(),
2136                ),
2137            },
2138            _ => unreachable!(),
2139        };
2140        if let Err(e) = ok {
2141            self.problem(path, attr.pos, format!("{}: {e}", attr.name));
2142        }
2143    }
2144}
2145
2146fn default_text() -> TextStyle {
2147    TextStyle {
2148        font: None,
2149        content: String::new(),
2150        size: 14.0,
2151        weight: 400,
2152        color: Color::fixed(Rgba {
2153            r: 0x0f,
2154            g: 0x17,
2155            b: 0x2a,
2156            a: 255,
2157        }),
2158        line_height: 1.4,
2159        align: TextAlign::Left,
2160    }
2161}
2162
2163fn view_box(s: &str) -> Result<[f32; 4], String> {
2164    let v: Vec<f32> = s
2165        .split([' ', ','])
2166        .filter(|p| !p.is_empty())
2167        .map(|p| parse::number(p).map(|n| n as f32))
2168        .collect::<Result<_, _>>()?;
2169    match v[..] {
2170        [x, y, w, h] if w > 0.0 && h > 0.0 => Ok([x, y, w, h]),
2171        _ => Err("expected \"minX minY width height\"".into()),
2172    }
2173}
2174
2175/// Every number in SVG path data `d` held to the same limits as a design's
2176/// other numbers. A number here is SVG's — it may have an exponent — and an
2177/// arc's two flags are single digits that may be written run together.
2178fn path_numbers(d: &str) -> Result<(), String> {
2179    let b = d.as_bytes();
2180    let mut i = 0;
2181    let mut command = b' ';
2182    // Which parameter of the current command comes next.
2183    let mut param = 0;
2184    while i < b.len() {
2185        let c = b[i];
2186        if c.is_ascii_alphabetic() && c != b'e' && c != b'E' {
2187            command = c;
2188            param = 0;
2189            i += 1;
2190            continue;
2191        }
2192        if c.is_ascii_whitespace() || c == b',' {
2193            i += 1;
2194            continue;
2195        }
2196        if matches!(command, b'A' | b'a') && matches!(param % 7, 3 | 4) && matches!(c, b'0' | b'1')
2197        {
2198            param += 1;
2199            i += 1;
2200            continue;
2201        }
2202        let start = i;
2203        if matches!(b[i], b'+' | b'-') {
2204            i += 1;
2205        }
2206        let mut dot = false;
2207        while i < b.len() && (b[i].is_ascii_digit() || (b[i] == b'.' && !dot)) {
2208            dot |= b[i] == b'.';
2209            i += 1;
2210        }
2211        if i < b.len() && matches!(b[i], b'e' | b'E') {
2212            let mut j = i + 1;
2213            if j < b.len() && matches!(b[j], b'+' | b'-') {
2214                j += 1;
2215            }
2216            if j < b.len() && b[j].is_ascii_digit() {
2217                i = j;
2218                while i < b.len() && b[i].is_ascii_digit() {
2219                    i += 1;
2220                }
2221            }
2222        }
2223        if i == start {
2224            // Not a number: the path parser says what is wrong.
2225            return Ok(());
2226        }
2227        let s = &d[start..i];
2228        if let Ok(n) = s.parse::<f64>() {
2229            parse::in_range(s, n)?;
2230        }
2231        param += 1;
2232    }
2233    Ok(())
2234}
2235
2236/// A path's own bounds, the view box of a `Vector` that names none.
2237fn bounds(d: &str) -> [f32; 4] {
2238    use kurbo::Shape;
2239    let b = kurbo::BezPath::from_svg(d)
2240        .map(|p| p.bounding_box())
2241        .unwrap_or_default();
2242    [
2243        b.x0 as f32,
2244        b.y0 as f32,
2245        (b.width() as f32).max(1.0),
2246        (b.height() as f32).max(1.0),
2247    ]
2248}
2249
2250fn interaction_ok(s: &str) -> bool {
2251    s == "back"
2252        || s == "close"
2253        || s.strip_prefix("navigate ")
2254            .or(s.strip_prefix("overlay "))
2255            .is_some_and(|p| !p.trim().is_empty())
2256}
2257
2258/// A path relative to a design's file, from the top of the served folder, or
2259/// `None` when it climbs out of the served folder.
2260pub fn join(from: &str, relative: &str) -> Option<String> {
2261    let mut parts: Vec<&str> = from.split('/').filter(|p| !p.is_empty()).collect();
2262    parts.pop();
2263    for part in relative.split('/') {
2264        match part {
2265            "." | "" => {}
2266            ".." => {
2267                parts.pop()?;
2268            }
2269            p => parts.push(p),
2270        }
2271    }
2272    Some(parts.join("/"))
2273}
2274
2275/// `value`, written in `path`, with a prop put in its place, and the file
2276/// that wrote what it ends up as.
2277fn substitute(value: &Value, props: &Props, path: &str) -> (Value, String) {
2278    match value {
2279        Value::Prop(p) => match props.get(p) {
2280            Some(given) => (given.value.clone(), given.by.clone()),
2281            None => (value.clone(), path.to_string()),
2282        },
2283        v => (v.clone(), path.to_string()),
2284    }
2285}
2286
2287/// Where a path in `attr`'s `value` was written: at the attribute, in
2288/// `path`, or — when the value came as a prop from another file, `by` — at
2289/// the string there.
2290fn written_at(path: &str, by: &str, attr: &parse::Attr, value: &Value) -> (String, Pos) {
2291    match value {
2292        Value::Str(_, pos) if by != path => (by.to_string(), *pos),
2293        _ => (path.to_string(), attr.pos),
2294    }
2295}
2296
2297fn show(v: &Value) -> String {
2298    match v {
2299        Value::Str(s, _) => format!("\"{s}\""),
2300        Value::Num(n) => n.to_string(),
2301        Value::Bool(b) => b.to_string(),
2302        Value::Array(_) => "an array".into(),
2303        Value::Object(_) => "an object".into(),
2304        Value::Prop(p) => p.clone(),
2305        Value::Theme(name, _) => format!("theme.{name}"),
2306        Value::Themed { written, .. } => written.clone(),
2307    }
2308}
2309
2310fn str_of(v: &Value) -> Option<String> {
2311    match v {
2312        Value::Str(s, _) => Some(s.clone()),
2313        _ => None,
2314    }
2315}
2316
2317/// Where `key` is written in a themes file's text: at the top level, or —
2318/// with `under` — inside that top-level key's block. YAML's values carry no
2319/// place, so a key is found by its text: on a line of its own, plain or in
2320/// quotes.
2321fn key_pos(text: &str, under: Option<&str>, key: &str) -> Option<Pos> {
2322    let names = |line: &str, key: &str| {
2323        let line = line.trim_start();
2324        [
2325            format!("{key}:"),
2326            format!("\"{key}\":"),
2327            format!("'{key}':"),
2328        ]
2329        .iter()
2330        .any(|k| line.starts_with(k.as_str()))
2331    };
2332    let top = |line: &str| !line.starts_with([' ', '\t']) && !line.trim().is_empty();
2333    let mut inside = under.is_none();
2334    for (n, line) in text.lines().enumerate() {
2335        if line.trim_start().starts_with('#') {
2336            continue;
2337        }
2338        let pos = Pos {
2339            line: n as u32 + 1,
2340            col: (line.len() - line.trim_start().len()) as u32 + 1,
2341        };
2342        match under {
2343            None if top(line) && names(line, key) => return Some(pos),
2344            None => {}
2345            Some(block) if top(line) => inside = names(line, block),
2346            Some(_) if inside && names(line, key) => return Some(pos),
2347            Some(_) => {}
2348        }
2349    }
2350    None
2351}
2352
2353/// The built-in elements; an import can't take one of their names.
2354const ELEMENTS: [&str; 6] = ["Frame", "Text", "Rect", "Ellipse", "Vector", "Image"];
2355
2356fn num(v: &Value) -> Result<f32, String> {
2357    match v {
2358        Value::Num(n) => Ok(*n as f32),
2359        other => Err(format!("expected a number, found {}", show(other))),
2360    }
2361}
2362
2363/// A length or a multiple that can't be negative: pixels, a size, a line height.
2364fn px(v: &Value) -> Result<f32, String> {
2365    match v {
2366        Value::Num(n) if *n >= 0.0 => Ok(*n as f32),
2367        Value::Num(n) => Err(format!("expected a number of 0 or more, found {n}")),
2368        other => Err(format!("expected a number, found {}", show(other))),
2369    }
2370}
2371
2372fn pxs(a: &[Value]) -> Result<Vec<f32>, String> {
2373    a.iter().map(px).collect()
2374}
2375
2376fn size(v: &Value) -> Result<Size, String> {
2377    match v {
2378        Value::Num(n) if *n >= 0.0 => Ok(Size::Px(*n as f32)),
2379        Value::Num(n) => Err(format!("expected pixels of 0 or more, found {n}")),
2380        Value::Str(s, _) if s == "hug" => Ok(Size::Hug),
2381        Value::Str(s, _) if s == "fill" => Ok(Size::Fill),
2382        other => Err(format!(
2383            "expected pixels, \"hug\" or \"fill\", found {}",
2384            show(other)
2385        )),
2386    }
2387}
2388
2389fn align(v: &Value, justify: bool) -> Result<Align, String> {
2390    match str_of(v).as_deref() {
2391        Some("start") => Ok(Align::Start),
2392        Some("center") => Ok(Align::Center),
2393        Some("end") => Ok(Align::End),
2394        Some("space-between") if justify => Ok(Align::SpaceBetween),
2395        _ if justify => Err("expected \"start\", \"center\", \"end\" or \"space-between\"".into()),
2396        _ => Err("expected \"start\", \"center\" or \"end\"".into()),
2397    }
2398}
2399
2400/// A color: a `#…` one, the same in both themes, or a theme color bound to
2401/// its file's themes.
2402fn color(v: &Value) -> Result<Color, String> {
2403    match v {
2404        Value::Str(s, _) => rgba(s).map(Color::fixed),
2405        Value::Themed { light, dark, .. } => Ok(Color {
2406            light: rgba(light)?,
2407            dark: rgba(dark)?,
2408        }),
2409        _ => Err(format!("expected a color, found {}", show(v))),
2410    }
2411}
2412
2413/// A `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa` color.
2414pub fn rgba(s: &str) -> Result<Rgba, String> {
2415    if s.starts_with("theme.") {
2416        return Err(format!(
2417            "{s} is a theme color: write it in braces, {{{s}}}, not in quotes"
2418        ));
2419    }
2420    let hex = s.strip_prefix('#').ok_or("expected a color like #rrggbb")?;
2421    if !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
2422        return Err(format!("{s} is not a color"));
2423    }
2424    let d = |i: usize, n: usize| {
2425        u8::from_str_radix(&hex[i..i + n], 16).map_err(|_| format!("{s} is not a color"))
2426    };
2427    let dd = |i: usize| d(i, 1).map(|v| v * 17);
2428    match hex.len() {
2429        3 => Ok(Rgba {
2430            r: dd(0)?,
2431            g: dd(1)?,
2432            b: dd(2)?,
2433            a: 255,
2434        }),
2435        4 => Ok(Rgba {
2436            r: dd(0)?,
2437            g: dd(1)?,
2438            b: dd(2)?,
2439            a: dd(3)?,
2440        }),
2441        6 => Ok(Rgba {
2442            r: d(0, 2)?,
2443            g: d(2, 2)?,
2444            b: d(4, 2)?,
2445            a: 255,
2446        }),
2447        8 => Ok(Rgba {
2448            r: d(0, 2)?,
2449            g: d(2, 2)?,
2450            b: d(4, 2)?,
2451            a: d(6, 2)?,
2452        }),
2453        _ => Err(format!("{s} is not a color")),
2454    }
2455}
2456
2457/// `c` written as `#rrggbbaa`.
2458fn hex(c: Rgba) -> String {
2459    format!("#{:02x}{:02x}{:02x}{:02x}", c.r, c.g, c.b, c.a)
2460}
2461
2462fn fill(v: &Value) -> Result<Fill, String> {
2463    match v {
2464        Value::Str(s, _) => fill_of(s),
2465        // Each theme's string differs only in its colors, so the two fills
2466        // have the same shape.
2467        Value::Themed { light, dark, .. } => match (fill_of(light)?, fill_of(dark)?) {
2468            (Fill::Solid(l), Fill::Solid(d)) => Ok(Fill::Solid(Color {
2469                light: l.light,
2470                dark: d.light,
2471            })),
2472            (
2473                Fill::Linear { angle, stops: l },
2474                Fill::Linear {
2475                    stops: d,
2476                    angle: dark_angle,
2477                },
2478            ) if angle == dark_angle && l.len() == d.len() => Ok(Fill::Linear {
2479                angle,
2480                stops: l
2481                    .iter()
2482                    .zip(&d)
2483                    .map(|((l, at), (d, _))| {
2484                        (
2485                            Color {
2486                                light: l.light,
2487                                dark: d.light,
2488                            },
2489                            *at,
2490                        )
2491                    })
2492                    .collect(),
2493            }),
2494            _ => Err(format!("{} is not the same fill in both themes", show(v))),
2495        },
2496        _ => Err(format!("expected a color or a gradient, found {}", show(v))),
2497    }
2498}
2499
2500/// A fill written as a string, its colors the same in both themes.
2501fn fill_of(s: &str) -> Result<Fill, String> {
2502    let Some(inner) = s
2503        .strip_prefix("linear-gradient(")
2504        .and_then(|r| r.strip_suffix(')'))
2505    else {
2506        return rgba(s).map(|c| Fill::Solid(Color::fixed(c)));
2507    };
2508    let wrong =
2509        || format!("expected \"linear-gradient(<angle>deg, <color> <stop>%, …)\", found \"{s}\"");
2510    let mut parts = inner.split(',').map(str::trim);
2511    let angle = parts
2512        .next()
2513        .and_then(|a| a.strip_suffix("deg"))
2514        .ok_or_else(wrong)?;
2515    let angle = parse::number(angle.trim())? as f32;
2516    let mut stops = Vec::new();
2517    for part in parts {
2518        let (c, at) = part.split_once(' ').ok_or_else(wrong)?;
2519        let at = at.trim().strip_suffix('%').ok_or_else(wrong)?;
2520        let at = parse::number(at)? as f32;
2521        stops.push((Color::fixed(rgba(c)?), at / 100.0));
2522    }
2523    if stops.len() < 2 {
2524        return Err(wrong());
2525    }
2526    Ok(Fill::Linear { angle, stops })
2527}
2528
2529/// Whether a value holds a theme color, at any depth.
2530fn has_theme(v: &Value) -> bool {
2531    match v {
2532        Value::Themed { .. } | Value::Theme(..) => true,
2533        Value::Array(items) => items.iter().any(has_theme),
2534        Value::Object(fields) => fields.iter().any(|(_, v)| has_theme(v)),
2535        _ => false,
2536    }
2537}
2538
2539fn shadow(v: &Value) -> Result<Shadow, String> {
2540    let Value::Object(fields) = v else {
2541        return Err("expected { x, y, blur, spread, color }".into());
2542    };
2543    let mut s = Shadow {
2544        x: 0.0,
2545        y: 0.0,
2546        blur: 0.0,
2547        spread: 0.0,
2548        color: Color::fixed(Rgba {
2549            r: 0,
2550            g: 0,
2551            b: 0,
2552            a: 64,
2553        }),
2554        at: Origin::default(),
2555    };
2556    for (k, v) in fields {
2557        match k.as_str() {
2558            "x" => s.x = num(v)?,
2559            "y" => s.y = num(v)?,
2560            "blur" => s.blur = num(v)?,
2561            "spread" => s.spread = num(v)?,
2562            "color" => s.color = color(v)?,
2563            other => return Err(format!("a shadow has no {other}")),
2564        }
2565    }
2566    Ok(s)
2567}
2568
2569mod again;
2570
2571pub use again::{Outcome, Resolution};
2572
2573#[cfg(test)]
2574mod tests;