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 crate::Problem;
9use crate::fonts::{self, Family, FontFile, FontFiles, FontId};
10use crate::parse::{self, Child, Element, Pos, TextPart, Value};
11use crate::scene::{
12    Align, Color, Files, Fill, Image, ImageFiles, ImageId, Kind, Layout, Node, Origin, Rgba, Scene,
13    Shadow, Size, TextAlign, TextStyle, Vector,
14};
15use crate::tree::NodeId;
16
17/// Resolves the design at `path`, reporting every problem found.
18pub fn resolve(files: &dyn Files, path: &str) -> Result<Scene, Vec<Problem>> {
19    resolved(files, path).map(|(scene, ..)| scene)
20}
21
22/// The design at `path` resolved, with the record of where each node came
23/// from and what it read — what resolving it again starts from — and the
24/// files it was made from.
25fn resolved(files: &dyn Files, path: &str) -> Result<(Scene, Record, Vec<String>), Vec<Problem>> {
26    let mut r = Resolver::new(files);
27    let root = r.design(path, &[], None, "", None);
28    if r.problems.is_empty() {
29        let scene = Scene::new(
30            root.expect("no problems means a root"),
31            r.images,
32            r.fonts,
33            r.themed,
34        );
35        Ok((scene, r.record, r.read))
36    } else {
37        Err(r.problems)
38    }
39}
40
41impl<'a> Resolver<'a> {
42    fn new(files: &'a dyn Files) -> Resolver<'a> {
43        Resolver {
44            files,
45            problems: Vec::new(),
46            read: Vec::new(),
47            stack: Vec::new(),
48            missing: Vec::new(),
49            depth: 0,
50            images: ImageFiles::new(),
51            by_path: HashMap::new(),
52            fonts: FontFiles::new(),
53            families: HashMap::new(),
54            themes: HashMap::new(),
55            themed: false,
56            instance: Vec::new(),
57            record: Record::default(),
58            current: None,
59            own_only: false,
60            elements: 0,
61        }
62    }
63}
64
65struct Resolver<'a> {
66    files: &'a dyn Files,
67    problems: Vec<Problem>,
68    read: Vec<String>,
69    stack: Vec<String>,
70    /// Imports already reported as `no such file`, so a use of one isn't
71    /// reported a second time.
72    missing: Vec<String>,
73    /// How many elements enclose the current one, across instances.
74    depth: usize,
75    /// Every image file the design shows, by id.
76    images: ImageFiles,
77    /// Each image file read so far, by its path.
78    by_path: HashMap<String, Result<ImageId, String>>,
79    /// Every font file the design's texts are set in, by id.
80    fonts: FontFiles,
81    /// Each font a `themes.yml` named and a text used so far, file or
82    /// folder, by its path, and whether its problem was reported yet: once
83    /// per resolve, however many texts use it.
84    families: HashMap<String, (Found, bool)>,
85    /// The themes each folder's designs use, merged from every `themes.yml`
86    /// from that folder up, by folder: read once per resolve.
87    themes: HashMap<String, Rc<Themes>>,
88    /// Whether a `theme.` reference reached a node.
89    themed: bool,
90    /// The element ids of the instances enclosing the element being
91    /// resolved, outermost first: the start of every id made here.
92    instance: Vec<u32>,
93    /// Where each node made came from and what it read.
94    record: Record,
95    /// The node whose attributes are being resolved: what each file read
96    /// now is read by.
97    current: Option<NodeId>,
98    /// Whether to resolve an element's own attributes only, leaving its
99    /// child elements alone: resolving one node again.
100    own_only: bool,
101    /// How many elements were resolved: the work resolving again counts.
102    elements: usize,
103}
104
105/// Where a resolved design's nodes came from and what they read: what
106/// [`Resolution`] resolves again from when a file changes.
107#[derive(Debug, Clone, Default)]
108struct Record {
109    /// Each instance, by the element ids leading to it — the root design's
110    /// is empty.
111    instances: BTreeMap<Vec<u32>, Instance>,
112    /// The instances of each design file.
113    by_file: HashMap<String, BTreeSet<Vec<u32>>>,
114    /// The nodes and instances that read each file — or folder, by its path
115    /// and a `/` — by id: a change to it resolves them again.
116    readers: HashMap<String, BTreeSet<Vec<u32>>>,
117    /// What each node or instance reads, so it can be forgotten.
118    reads: BTreeMap<Vec<u32>, BTreeSet<String>>,
119    /// The nodes a `theme.` reference reached.
120    themed: BTreeSet<NodeId>,
121    /// The parse each design file was resolved from.
122    parses: HashMap<String, Arc<parse::File>>,
123}
124
125impl Record {
126    /// `id`, a node's or an instance's, reads `key`.
127    fn read_by(&mut self, key: &str, id: &[u32]) {
128        self.readers
129            .entry(key.to_string())
130            .or_default()
131            .insert(id.to_vec());
132        self.reads
133            .entry(id.to_vec())
134            .or_default()
135            .insert(key.to_string());
136    }
137
138    /// `id` reads nothing any more.
139    fn forget(&mut self, id: &[u32]) {
140        if let Some(keys) = self.reads.remove(id) {
141            for key in keys {
142                if let Some(readers) = self.readers.get_mut(&key) {
143                    readers.remove(id);
144                    if readers.is_empty() {
145                        self.readers.remove(&key);
146                    }
147                }
148            }
149        }
150    }
151}
152
153/// One use of a design file: the props its elements are resolved in, its
154/// imports, the layout of the frame it sits in, the caller's attributes laid
155/// over its root, and its root node's id.
156#[derive(Debug, Clone)]
157struct Instance {
158    file: String,
159    props: Arc<Props>,
160    imports: Arc<HashMap<String, String>>,
161    parent: Option<Layout>,
162    caller: Option<Caller>,
163    root: NodeId,
164}
165
166/// The file an instance is used in, and the attributes it lays over the
167/// instance's root — `x`, `y`, `width` and the like — as given there.
168#[derive(Debug, Clone)]
169struct Caller {
170    file: String,
171    overrides: Vec<(parse::Attr, String)>,
172}
173
174/// Whether a value as written names a theme color.
175fn writes_theme(v: &Value) -> bool {
176    match v {
177        Value::Theme(..) => true,
178        Value::Str(s, _) => s.starts_with("linear-gradient(") && s.contains("theme."),
179        Value::Array(items) => items.iter().any(writes_theme),
180        Value::Object(fields) => fields.iter().any(|(_, v)| writes_theme(v)),
181        _ => false,
182    }
183}
184
185/// Every `themes.yml` a design file at `path` reads its themes from: its
186/// folder's and each folder's above it.
187fn themes_chain(path: &str) -> Vec<String> {
188    let mut out = Vec::new();
189    let mut folder = path.rsplit_once('/').map(|(f, _)| f);
190    while let Some(f) = folder {
191        out.push(format!("{f}/{THEMES_FILE}"));
192        folder = f.rsplit_once('/').map(|(f, _)| f);
193    }
194    out.push(THEMES_FILE.to_string());
195    out
196}
197
198/// The themes a folder's designs use: every `themes.yml` with `kind: themes`
199/// from the top of the served folder down to it, merged — a lower file's
200/// names over a higher one's, theme by theme.
201#[derive(Debug, Clone, Default)]
202struct Themes {
203    /// Whether any such file was found.
204    found: bool,
205    light: BTreeMap<String, Rgba>,
206    dark: BTreeMap<String, Rgba>,
207    /// The fonts the files name, by name, a lower file's over a higher one's.
208    fonts: BTreeMap<String, ThemesFont>,
209}
210
211/// A font a `themes.yml`'s `fonts` names: its path as written, its address —
212/// `None` when it leaves the served folder — and where it is written.
213#[derive(Debug, Clone)]
214struct ThemesFont {
215    written: String,
216    at: Option<String>,
217    origin: Origin,
218}
219
220/// One `themes.yml`'s two themes and the fonts it names, as far as they are
221/// valid.
222struct ThemesFile {
223    light: BTreeMap<String, Rgba>,
224    dark: BTreeMap<String, Rgba>,
225    fonts: BTreeMap<String, ThemesFont>,
226}
227
228/// The file a folder's themes are in.
229pub const THEMES_FILE: &str = "themes.yml";
230
231/// How deep a resolved design's tree may nest, counting through instances:
232/// the parser's own limit for one file, and so far beyond any real design.
233const MAX_DEPTH: usize = parse::MAX_DEPTH;
234
235/// A prop's value and the file that wrote it, which a path in it is
236/// relative to.
237#[derive(Debug, Clone)]
238struct Given {
239    value: Value,
240    by: String,
241}
242
243type Props = HashMap<String, Given>;
244
245/// A font a `font` names: its family, or why it can't be used; `None` when
246/// there is nothing there.
247type Found = Option<Result<Rc<Family>, String>>;
248
249impl Resolver<'_> {
250    /// The node being resolved reads the file `key` — a folder's listing
251    /// when it ends in `/`: a change to it resolves the node again.
252    fn read_now(&mut self, key: &str) {
253        if let Some(node) = &self.current {
254            let node = node.0.clone();
255            self.record.read_by(key, &node);
256        }
257    }
258
259    fn problem(&mut self, file: &str, pos: Pos, message: String) {
260        self.problems.push(Problem {
261            file: file.to_string(),
262            pos,
263            message,
264        });
265    }
266
267    /// Binds every theme color written in `el` and the elements in it — in
268    /// `path`'s file — to that file's themes.
269    fn bind_element(&mut self, path: &str, el: &mut Element) {
270        self.bind_own(path, el);
271        for child in &mut el.children {
272            if let Child::Element(e) = child {
273                self.bind_element(path, e);
274            }
275        }
276    }
277
278    /// Binds the theme colors written in `el`'s own attributes, the node or
279    /// instance it makes reading `path`'s themes when there are any.
280    fn bind_own(&mut self, path: &str, el: &mut Element) {
281        let themed = el.attrs.iter().any(|a| writes_theme(&a.value));
282        let props_themed = el
283            .attrs
284            .iter()
285            .any(|a| a.name == "props" && writes_theme(&a.value));
286        for attr in &mut el.attrs {
287            self.bind(path, &mut attr.value);
288        }
289        if themed {
290            let mut id = self.instance.clone();
291            id.push(el.id);
292            for file in themes_chain(path) {
293                self.record.read_by(&file, &id);
294            }
295        }
296        // A theme color among a design's `props` defaults reaches whatever
297        // reads that prop, anywhere in the design: the instance reads the
298        // themes, so a change to them resolves it again with its subtree.
299        if props_themed {
300            let instance = self.instance.clone();
301            for file in themes_chain(path) {
302                self.record.read_by(&file, &instance);
303            }
304        }
305    }
306
307    /// Binds a value written in `path` to `path`'s themes: `theme.name` to
308    /// its two colors, and a gradient naming theme colors to one gradient
309    /// per theme.
310    fn bind(&mut self, path: &str, value: &mut Value) {
311        match value {
312            Value::Theme(name, pos) => {
313                // A problem already when there is none; any color keeps
314                // the rest of the design checked.
315                let (light, dark) = self
316                    .theme_color(path, name, *pos)
317                    .map_or(("#0000".into(), "#0000".into()), |(l, d)| (hex(l), hex(d)));
318                *value = Value::Themed {
319                    light,
320                    dark,
321                    written: format!("theme.{name}"),
322                };
323            }
324            Value::Str(s, pos) if s.starts_with("linear-gradient(") && s.contains("theme.") => {
325                let (mut light, mut dark) = (String::new(), String::new());
326                let mut rest = s.as_str();
327                let mut col = pos.col + 1;
328                while let Some(at) = rest.find("theme.") {
329                    let before = &rest[..at];
330                    light.push_str(before);
331                    dark.push_str(before);
332                    col += before.chars().count() as u32;
333                    let after = &rest[at + "theme.".len()..];
334                    let len = after
335                        .find(|c: char| !(c.is_alphanumeric() || c == '_' || c == '-'))
336                        .unwrap_or(after.len());
337                    let name = &after[..len];
338                    let at_ref = Pos {
339                        line: pos.line,
340                        col,
341                    };
342                    match self.theme_color(path, name, at_ref) {
343                        Some((l, d)) => {
344                            light.push_str(&hex(l));
345                            dark.push_str(&hex(d));
346                        }
347                        // A problem already; any color keeps the rest checked.
348                        None => {
349                            light.push_str("#0000");
350                            dark.push_str("#0000");
351                        }
352                    }
353                    col += ("theme.".len() + name.chars().count()) as u32;
354                    rest = &after[len..];
355                }
356                light.push_str(rest);
357                dark.push_str(rest);
358                *value = Value::Themed {
359                    light,
360                    dark,
361                    written: format!("\"{s}\""),
362                };
363            }
364            Value::Array(items) => {
365                for item in items {
366                    self.bind(path, item);
367                }
368            }
369            Value::Object(fields) => {
370                for (_, v) in fields {
371                    self.bind(path, v);
372                }
373            }
374            _ => {}
375        }
376    }
377
378    /// The two colors of `theme.name`, written at `pos` in `path`, from the
379    /// themes of `path`'s folder — or `None`, with the problem reported.
380    fn theme_color(&mut self, path: &str, name: &str, pos: Pos) -> Option<(Rgba, Rgba)> {
381        let folder = path.rsplit_once('/').map_or("", |(folder, _)| folder);
382        let themes = self.themes(folder);
383        let problem = if !themes.found {
384            format!(
385                "theme.{name}: no {THEMES_FILE} with kind: themes in this design's folder or above it"
386            )
387        } else {
388            match (themes.light.get(name), themes.dark.get(name)) {
389                (Some(l), Some(d)) => return Some((*l, *d)),
390                (None, None) => {
391                    let mut names: Vec<&String> =
392                        themes.light.keys().chain(themes.dark.keys()).collect();
393                    names.sort();
394                    names.dedup();
395                    let names: Vec<&str> = names.iter().map(|n| n.as_str()).collect();
396                    if names.is_empty() {
397                        format!("theme.{name}: the themes have no colors")
398                    } else {
399                        format!(
400                            "theme.{name}: the themes have no {name}; they have {}",
401                            names.join(", ")
402                        )
403                    }
404                }
405                (Some(_), None) => format!(
406                    "theme.{name}: only the light theme has {name}; a name needs a color in both"
407                ),
408                (None, Some(_)) => format!(
409                    "theme.{name}: only the dark theme has {name}; a name needs a color in both"
410                ),
411            }
412        };
413        self.problem(path, pos, problem);
414        None
415    }
416
417    /// The themes the designs in `folder` use: its own `themes.yml` over
418    /// those of the folders above it, each read once per resolve — a missing
419    /// one too, so that creating it counts as a change.
420    fn themes(&mut self, folder: &str) -> Rc<Themes> {
421        if let Some(themes) = self.themes.get(folder) {
422            return themes.clone();
423        }
424        let above = match folder.rsplit_once('/') {
425            Some((parent, _)) => self.themes(parent),
426            None if folder.is_empty() => Rc::new(Themes::default()),
427            None => self.themes(""),
428        };
429        let file = if folder.is_empty() {
430            THEMES_FILE.to_string()
431        } else {
432            format!("{folder}/{THEMES_FILE}")
433        };
434        let themes = match self.themes_file(&file) {
435            None => above,
436            Some(own) => {
437                let mut merged = (*above).clone();
438                merged.found = true;
439                merged.light.extend(own.light);
440                merged.dark.extend(own.dark);
441                merged.fonts.extend(own.fonts);
442                Rc::new(merged)
443            }
444        };
445        self.themes.insert(folder.to_string(), themes.clone());
446        themes
447    }
448
449    /// The themes in the file at `path`, as far as they are valid, with its
450    /// problems reported; `None` when there is no such file or it isn't a
451    /// themes file — its `kind` isn't `themes`.
452    fn themes_file(&mut self, path: &str) -> Option<ThemesFile> {
453        let text = self.files.read(path)?;
454        if !self.read.iter().any(|p| p == path) {
455            self.read.push(path.to_string());
456        }
457        let mut file = ThemesFile {
458            light: BTreeMap::new(),
459            dark: BTreeMap::new(),
460            fonts: BTreeMap::new(),
461        };
462        let yaml: serde_yaml::Value = match serde_yaml::from_str(&text) {
463            Ok(yaml) => yaml,
464            Err(e) => {
465                // Only a file that says it is a themes file is ours to report.
466                if !text.lines().any(|l| l.trim() == "kind: themes") {
467                    return None;
468                }
469                let pos = e.location().map_or(Pos { line: 1, col: 1 }, |l| Pos {
470                    line: l.line() as u32,
471                    col: l.column() as u32,
472                });
473                self.problem(path, pos, format!("this themes file isn't valid YAML: {e}"));
474                return Some(file);
475            }
476        };
477        let serde_yaml::Value::Mapping(top) = yaml else {
478            return None;
479        };
480        if top.get("kind").and_then(|k| k.as_str()) != Some("themes") {
481            return None;
482        }
483        let start = Pos { line: 1, col: 1 };
484        for (key, value) in &top {
485            let key = key.as_str().unwrap_or_default();
486            let theme = match key {
487                "kind" => continue,
488                "light" => &mut file.light,
489                "dark" => &mut file.dark,
490                "fonts" => {
491                    self.themes_fonts(path, &text, value, &mut file.fonts);
492                    continue;
493                }
494                other => {
495                    let pos = key_pos(&text, None, other).unwrap_or(start);
496                    self.problem(
497                        path,
498                        pos,
499                        format!("{other}: a themes file has kind, fonts, light and dark only"),
500                    );
501                    continue;
502                }
503            };
504            let at = key_pos(&text, None, key).unwrap_or(start);
505            let serde_yaml::Value::Mapping(colors) = value else {
506                self.problem(
507                    path,
508                    at,
509                    format!("{key}: expected a map of names to colors, like surface: \"#ffffff\""),
510                );
511                continue;
512            };
513            for (name, color) in colors {
514                let Some(name) = name.as_str() else {
515                    self.problem(path, at, format!("{key}: a color's name is text"));
516                    continue;
517                };
518                let pos = key_pos(&text, Some(key), name).unwrap_or(at);
519                if name.is_empty()
520                    || name.starts_with(|c: char| c.is_ascii_digit() || c == '-')
521                    || !name
522                        .chars()
523                        .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
524                {
525                    self.problem(
526                        path,
527                        pos,
528                        format!(
529                            "{key}.{name}: a name holds letters, digits, _ and -, and starts with a letter"
530                        ),
531                    );
532                    continue;
533                }
534                let value = match color {
535                    serde_yaml::Value::String(s) => rgba(s),
536                    serde_yaml::Value::Null => Err(
537                        "no color: write it in quotes, \"#rrggbb\", as YAML reads # as a comment"
538                            .to_string(),
539                    ),
540                    _ => Err("expected a color like \"#rrggbb\"".to_string()),
541                };
542                match value {
543                    Ok(c) => {
544                        theme.insert(name.to_string(), c);
545                    }
546                    Err(e) => self.problem(path, pos, format!("{key}.{name}: {e}")),
547                }
548            }
549        }
550        for needed in ["light", "dark"] {
551            if !top.contains_key(needed) {
552                self.problem(
553                    path,
554                    start,
555                    format!("a themes file has a light and a dark theme; this one has no {needed}"),
556                );
557            }
558        }
559        Some(file)
560    }
561
562    /// The fonts a `themes.yml` at `path` names in `value`, its `fonts`, each
563    /// a name and a path from that file; a bad entry is a problem at its
564    /// line.
565    fn themes_fonts(
566        &mut self,
567        path: &str,
568        text: &str,
569        value: &serde_yaml::Value,
570        into: &mut BTreeMap<String, ThemesFont>,
571    ) {
572        let start = Pos { line: 1, col: 1 };
573        let at = key_pos(text, None, "fonts").unwrap_or(start);
574        let serde_yaml::Value::Mapping(entries) = value else {
575            self.problem(
576                path,
577                at,
578                "fonts: expected a map of names to font files or folders, like figtree: ./fonts/Figtree".into(),
579            );
580            return;
581        };
582        for (name, written) in entries {
583            let Some(name) = name.as_str() else {
584                self.problem(path, at, "fonts: a font's name is text".into());
585                continue;
586            };
587            let pos = key_pos(text, Some("fonts"), name).unwrap_or(at);
588            if name.is_empty()
589                || !name
590                    .chars()
591                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
592            {
593                self.problem(
594                    path,
595                    pos,
596                    format!(
597                        "fonts.{name}: a font's name holds lowercase letters, digits and - only"
598                    ),
599                );
600                continue;
601            }
602            let Some(written) = written.as_str() else {
603                self.problem(
604                    path,
605                    pos,
606                    format!("fonts.{name}: expected a path to a font file or a folder of one family's, like ./fonts/Figtree"),
607                );
608                continue;
609            };
610            into.insert(
611                name.to_string(),
612                ThemesFont {
613                    written: written.to_string(),
614                    at: join(path, written),
615                    origin: Origin {
616                        file: path.to_string(),
617                        pos,
618                    },
619                },
620            );
621        }
622    }
623
624    /// A design file's imports, each to the file it names, and its root's
625    /// declared props — with the problems of either reported.
626    /// `None` when its root isn't a `Frame`.
627    fn scope(
628        &mut self,
629        path: &str,
630        file: &parse::File,
631    ) -> Option<(HashMap<String, String>, Props)> {
632        let mut imports = HashMap::new();
633        for import in &file.imports {
634            if import.name == parse::THEME {
635                self.problem(
636                    path,
637                    import.pos,
638                    format!(
639                        "import {0} — {0} is the name of the design's themes",
640                        import.name
641                    ),
642                );
643                continue;
644            }
645            if ELEMENTS.contains(&import.name.as_str()) {
646                self.problem(
647                    path,
648                    import.pos,
649                    format!("import {0} — {0} is a built-in element", import.name),
650                );
651                continue;
652            }
653            if imports.contains_key(&import.name) {
654                self.problem(
655                    path,
656                    import.pos,
657                    format!("import {0} — {0} is already imported", import.name),
658                );
659                continue;
660            }
661            if !import.path.ends_with(".design") {
662                self.problem(
663                    path,
664                    import.pos,
665                    format!("import {} — a design is a .design file", import.path),
666                );
667                continue;
668            }
669            match join(path, &import.path) {
670                Some(target) => {
671                    // Every import must exist, used or not.
672                    if self.files.read(&target).is_none() {
673                        self.problem(
674                            path,
675                            import.pos,
676                            format!("import {} — no such file", import.path),
677                        );
678                        self.missing.push(target.clone());
679                    }
680                    imports.insert(import.name.clone(), target);
681                }
682                None => self.problem(
683                    path,
684                    import.pos,
685                    format!("import {} — outside the served folder", import.path),
686                ),
687            }
688        }
689        if file.root.name != "Frame" {
690            self.problem(
691                path,
692                file.root.pos,
693                format!("the root element must be a Frame, not {}", file.root.name),
694            );
695            return None;
696        }
697        // Props: the root's declared defaults, overridden by the instance's.
698        let mut props = Props::new();
699        if let Some(attr) = file.root.attrs.iter().find(|a| a.name == "props") {
700            match &attr.value {
701                Value::Object(fields) => {
702                    for (k, v) in fields {
703                        if k == parse::THEME {
704                            self.problem(
705                                path,
706                                attr.pos,
707                                format!(
708                                    "props: {k} is the name of the design's themes, not a prop"
709                                ),
710                            );
711                            continue;
712                        }
713                        props.insert(
714                            k.clone(),
715                            Given {
716                                value: v.clone(),
717                                by: path.to_string(),
718                            },
719                        );
720                    }
721                }
722                _ => self.problem(
723                    path,
724                    attr.pos,
725                    "props: expected an object of defaults".into(),
726                ),
727            }
728        }
729        Some((imports, props))
730    }
731
732    /// The root node of the design at `path`, with `args` given to its props.
733    fn design(
734        &mut self,
735        path: &str,
736        args: &[(parse::Attr, String)],
737        from: Option<(&str, Pos)>,
738        used_as: &str,
739        parent: Option<Layout>,
740    ) -> Option<Node> {
741        if self.stack.iter().any(|p| p == path) {
742            let (file, pos) = from.unwrap_or((path, Pos { line: 1, col: 1 }));
743            self.problem(file, pos, format!("{path} uses itself"));
744            return None;
745        }
746        let Some(source) = self.files.read(path) else {
747            if from.is_some() && self.missing.iter().any(|p| p == path) {
748                return None;
749            }
750            let (file, pos) = from.unwrap_or((path, Pos { line: 1, col: 1 }));
751            self.problem(file, pos, format!("{path} — no such file"));
752            return None;
753        };
754        if !self.read.iter().any(|p| p == path) {
755            self.read.push(path.to_string());
756        }
757        let mut file = match self.files.parse(path, &source) {
758            Ok(file) => {
759                self.record.parses.insert(path.to_string(), file.clone());
760                (*file).clone()
761            }
762            Err(mut problem) => {
763                problem.file = path.to_string();
764                self.problems.push(problem);
765                return None;
766            }
767        };
768        // Bound now, before any value leaves this file as a prop: a theme
769        // color is this file's, wherever it is used.
770        self.bind_element(path, &mut file.root);
771        let (imports, mut props) = self.scope(path, &file)?;
772        let (caller, caller_pos) = from.unwrap_or((path, file.root.pos));
773        let caller = caller.to_string();
774        let mut overrides = Vec::new();
775        for (arg, by) in args {
776            if matches!(
777                arg.name.as_str(),
778                "x" | "y" | "width" | "height" | "name" | "opacity" | "onClick"
779            ) {
780                overrides.push((arg.clone(), by.clone()));
781            } else if props.contains_key(&arg.name) {
782                props.insert(
783                    arg.name.clone(),
784                    Given {
785                        value: arg.value.clone(),
786                        by: by.clone(),
787                    },
788                );
789            } else {
790                self.problem(
791                    &caller,
792                    arg.pos,
793                    format!("{used_as} has no prop \"{}\"", arg.name),
794                );
795            }
796        }
797        let _ = caller_pos;
798        let key = self.instance.clone();
799        for target in imports.values() {
800            self.record.read_by(target, &key);
801        }
802        self.record.instances.insert(
803            key.clone(),
804            Instance {
805                file: path.to_string(),
806                props: Arc::new(props.clone()),
807                imports: Arc::new(imports.clone()),
808                parent,
809                caller: from.map(|_| Caller {
810                    file: caller.clone(),
811                    overrides: overrides.clone(),
812                }),
813                root: {
814                    let mut id = key.clone();
815                    id.push(file.root.id);
816                    NodeId(id)
817                },
818            },
819        );
820        self.record
821            .by_file
822            .entry(path.to_string())
823            .or_default()
824            .insert(key);
825        self.stack.push(path.to_string());
826        let mut root = self.element(path, &file.root, &props, &imports, true, parent);
827        self.stack.pop();
828        if let Some(root) = root.as_mut() {
829            self.current = Some(root.id.clone());
830            for (attr, by) in &overrides {
831                // The caller's own props are already substituted into these.
832                self.common(&caller, by, root, attr, attr.value.clone());
833                self.fill_needs_layout(&caller, attr, &attr.value, parent);
834                self.place_needs_no_layout(&caller, attr, parent);
835            }
836        }
837        root
838    }
839
840    fn element(
841        &mut self,
842        path: &str,
843        el: &Element,
844        props: &Props,
845        imports: &HashMap<String, String>,
846        root: bool,
847        parent: Option<Layout>,
848    ) -> Option<Node> {
849        if self.depth >= MAX_DEPTH {
850            self.problem(path, el.pos, format!("nested more than {MAX_DEPTH} deep"));
851            return None;
852        }
853        self.depth += 1;
854        self.elements += 1;
855        let node = self.element_inner(path, el, props, imports, root, parent);
856        self.depth -= 1;
857        node
858    }
859
860    fn element_inner(
861        &mut self,
862        path: &str,
863        el: &Element,
864        props: &Props,
865        imports: &HashMap<String, String>,
866        root: bool,
867        parent: Option<Layout>,
868    ) -> Option<Node> {
869        // An attribute given twice is a problem, on an element or an instance
870        // alike; the first one counts.
871        let mut seen: Vec<&str> = Vec::new();
872        let mut attrs = Vec::new();
873        for attr in &el.attrs {
874            if seen.contains(&attr.name.as_str()) {
875                self.problem(path, attr.pos, format!("{} is given twice", attr.name));
876            } else {
877                seen.push(&attr.name);
878                attrs.push(attr);
879            }
880        }
881        if let Some(target) = imports.get(&el.name) {
882            if !el.children.is_empty() {
883                self.problem(path, el.pos, format!("{} takes no children", el.name));
884            }
885            let args: Vec<(parse::Attr, String)> = attrs
886                .iter()
887                .filter(|a| a.name != "key" || !self.key_ok(path, a))
888                .map(|a| {
889                    let (value, by) = substitute(&a.value, props, path);
890                    let arg = parse::Attr {
891                        name: a.name.clone(),
892                        value,
893                        pos: a.pos,
894                    };
895                    (arg, by)
896                })
897                .collect();
898            let target = target.clone();
899            self.instance.push(el.id);
900            let node = self.design(&target, &args, Some((path, el.pos)), &el.name, parent);
901            self.instance.pop();
902            return node;
903        }
904        let kind = match el.name.as_str() {
905            // Keep in step with ELEMENTS.
906            "Frame" => Kind::Frame,
907            "Text" => Kind::Text,
908            "Rect" => Kind::Rect,
909            "Ellipse" => Kind::Ellipse,
910            "Vector" => Kind::Vector,
911            "Image" => Kind::Image,
912            other => {
913                self.problem(path, el.pos, format!("unknown element {other}"));
914                return None;
915            }
916        };
917        let mut node = Node::new(kind, &el.name);
918        let mut id = self.instance.clone();
919        id.push(el.id);
920        node.id = NodeId(id);
921        self.current = Some(node.id.clone());
922        // A text's `font`, as given and by the file that wrote it, chosen a
923        // face from once its weight is known.
924        let mut font = None;
925        for &attr in &attrs {
926            if attr.name == "props" {
927                if !root {
928                    self.problem(
929                        path,
930                        attr.pos,
931                        "props: only a design's root declares props".into(),
932                    );
933                }
934                continue;
935            }
936            // Shown on its own a design has no parent to place it in, and used
937            // as an instance it goes where the instance's own x and y say.
938            if root && matches!(attr.name.as_str(), "x" | "y") {
939                self.problem(
940                    path,
941                    attr.pos,
942                    format!(
943                        "{}: a design's root has no place of its own; place an instance with x and y where it is used",
944                        attr.name
945                    ),
946                );
947                continue;
948            }
949            let (value, by) = match &attr.value {
950                Value::Prop(p) => match props.get(p) {
951                    Some(given) => (given.value.clone(), given.by.clone()),
952                    None => {
953                        self.problem(path, attr.pos, format!("{}: no prop \"{p}\"", attr.name));
954                        continue;
955                    }
956                },
957                v => (v.clone(), path.to_string()),
958            };
959            if has_theme(&value) {
960                self.themed = true;
961                self.record.themed.insert(node.id.clone());
962            }
963            self.fill_needs_layout(path, attr, &value, parent);
964            self.place_needs_no_layout(path, attr, parent);
965            if kind == Kind::Text && attr.name == "font" {
966                font = Some((attr, value, by));
967                continue;
968            }
969            self.attribute(path, &by, &mut node, attr, value);
970        }
971        match kind {
972            Kind::Vector if !el.attrs.iter().any(|a| a.name == "d") => self.problem(
973                path,
974                el.pos,
975                format!("{} needs a path, d=\"M…\"", node.name),
976            ),
977            Kind::Image if node.image.is_none() && !el.attrs.iter().any(|a| a.name == "src") => {
978                self.problem(
979                    path,
980                    el.pos,
981                    format!("{} needs a file, src=\"…\"", node.name),
982                )
983            }
984            _ => {}
985        }
986        if kind == Kind::Text {
987            let mut content = String::new();
988            for child in &el.children {
989                match child {
990                    Child::Text(parts, pos) => {
991                        for part in parts {
992                            match part {
993                                TextPart::Literal(s) => content.push_str(s),
994                                TextPart::Prop(p) => match props.get(p).map(|g| &g.value) {
995                                    Some(Value::Str(s, _)) => content.push_str(s),
996                                    Some(Value::Num(n)) => content.push_str(&n.to_string()),
997                                    _ => self.problem(path, *pos, format!("no text prop \"{p}\"")),
998                                },
999                                TextPart::Theme(name, at) => self.problem(
1000                                    path,
1001                                    *at,
1002                                    format!(
1003                                        "theme.{name} is a color, not text: give it to color={{theme.{name}}}"
1004                                    ),
1005                                ),
1006                            }
1007                        }
1008                    }
1009                    Child::Element(e) => self.problem(path, e.pos, "Text holds text only".into()),
1010                }
1011            }
1012            let text = node.text.get_or_insert_with(default_text);
1013            text.content = content;
1014            let weight = text.weight;
1015            let weight_at = attrs.iter().find(|a| a.name == "weight").map(|a| a.pos);
1016            text.font = self.text_font(path, el.pos, font, weight, weight_at);
1017        } else {
1018            let mut keys: HashMap<&str, Pos> = HashMap::new();
1019            for child in &el.children {
1020                if let Child::Element(e) = child
1021                    && let Some(k) = crate::ids::key(e)
1022                    && keys.insert(k, e.pos).is_some()
1023                {
1024                    let at = e
1025                        .attrs
1026                        .iter()
1027                        .find(|a| a.name == "key")
1028                        .map_or(e.pos, |a| a.pos);
1029                    self.problem(path, at, format!("key: {k:?} is another sibling's key too"));
1030                }
1031                match child {
1032                    Child::Element(_) if self.own_only => {}
1033                    Child::Element(e) => {
1034                        if kind != Kind::Frame {
1035                            self.problem(path, e.pos, format!("{} takes no children", el.name));
1036                        } else if let Some(n) =
1037                            self.element(path, e, props, imports, false, Some(node.layout))
1038                        {
1039                            node.children.push(n);
1040                            self.current = Some(node.id.clone());
1041                        }
1042                    }
1043                    Child::Text(_, pos) => self.problem(
1044                        path,
1045                        *pos,
1046                        format!("text belongs in a Text, not a {}", el.name),
1047                    ),
1048                }
1049            }
1050        }
1051        Some(node)
1052    }
1053
1054    /// `"fill"` takes the free space of a parent's auto layout, so a child of a
1055    /// frame with no layout can't have it.
1056    fn fill_needs_layout(
1057        &mut self,
1058        path: &str,
1059        attr: &parse::Attr,
1060        value: &Value,
1061        parent: Option<Layout>,
1062    ) {
1063        if parent == Some(Layout::None)
1064            && matches!(attr.name.as_str(), "width" | "height")
1065            && matches!(value, Value::Str(s, _) if s == "fill")
1066        {
1067            self.problem(
1068                path,
1069                attr.pos,
1070                format!(
1071                    "{}: \"fill\" needs a parent with layout=\"row\" or \"column\"",
1072                    attr.name
1073                ),
1074            );
1075        }
1076    }
1077
1078    /// `x` and `y` place a child inside a frame with no layout; auto layout
1079    /// places its children itself, so there they would mean nothing.
1080    fn place_needs_no_layout(&mut self, path: &str, attr: &parse::Attr, parent: Option<Layout>) {
1081        if matches!(parent, Some(Layout::Row | Layout::Column))
1082            && matches!(attr.name.as_str(), "x" | "y")
1083        {
1084            self.problem(
1085                path,
1086                attr.pos,
1087                format!(
1088                    "{}: places a child only in a frame with layout=\"none\"",
1089                    attr.name
1090                ),
1091            );
1092        }
1093    }
1094
1095    // Each arm sets a field and says whether the value fit: `Ok(field = v)`.
1096    #[allow(clippy::unit_arg)]
1097    fn attribute(
1098        &mut self,
1099        path: &str,
1100        by: &str,
1101        node: &mut Node,
1102        attr: &parse::Attr,
1103        value: Value,
1104    ) {
1105        let kind = node.kind;
1106        // A path is relative to the file that wrote it, and its problem is
1107        // where it was written.
1108        let (at_file, at_pos) = written_at(path, by, attr, &value);
1109        let ok = match (attr.name.as_str(), kind) {
1110            ("x" | "y" | "width" | "height" | "name" | "opacity" | "onClick", _) => {
1111                self.common(path, by, node, attr, value);
1112                return;
1113            }
1114            ("key", _) => {
1115                self.key_ok(path, attr);
1116                return;
1117            }
1118            ("fill", k) if k != Kind::Text => fill(&value).map(|f| node.fill = Some(f)),
1119            ("stroke", k) if k != Kind::Text => color(&value).map(|c| node.stroke = Some(c)),
1120            ("strokeWidth", k) if k != Kind::Text => px(&value).map(|n| node.stroke_width = n),
1121            ("radius", k) if k != Kind::Text => match &value {
1122                Value::Num(_) => px(&value).map(|n| node.radius = [n; 4]),
1123                Value::Array(a) if a.len() == 4 => {
1124                    pxs(a).map(|v| node.radius = [v[0], v[1], v[2], v[3]])
1125                }
1126                _ => Err("expected pixels or an array of 4".into()),
1127            },
1128            ("shadow", k) if k != Kind::Text => shadow(&value).map(|mut s| {
1129                s.at = Origin {
1130                    file: path.to_string(),
1131                    pos: attr.pos,
1132                };
1133                node.shadow = Some(s)
1134            }),
1135            // Only a frame has children to clip.
1136            ("clip", Kind::Frame) => match value {
1137                Value::Bool(b) => Ok(node.clip = b),
1138                _ => Err("expected true or false".into()),
1139            },
1140            ("layout", Kind::Frame) => match str_of(&value).as_deref() {
1141                Some("row") => Ok(node.layout = Layout::Row),
1142                Some("column") => Ok(node.layout = Layout::Column),
1143                Some("none") => Ok(node.layout = Layout::None),
1144                _ => Err("expected \"row\", \"column\" or \"none\"".into()),
1145            },
1146            ("gap", Kind::Frame) => px(&value).map(|n| node.gap = n),
1147            ("padding", Kind::Frame) => match &value {
1148                Value::Num(_) => px(&value).map(|n| node.padding = [n; 4]),
1149                Value::Array(a) if a.len() == 2 => {
1150                    pxs(a).map(|v| node.padding = [v[0], v[1], v[0], v[1]])
1151                }
1152                Value::Array(a) if a.len() == 4 => {
1153                    pxs(a).map(|v| node.padding = [v[0], v[1], v[2], v[3]])
1154                }
1155                other => Err(format!(
1156                    "expected pixels or an array of 2 or 4, found {}",
1157                    show(other)
1158                )),
1159            },
1160            ("align", Kind::Frame) => align(&value, false).map(|a| node.align = a),
1161            ("justify", Kind::Frame) => align(&value, true).map(|a| node.justify = a),
1162            ("size", Kind::Text) => {
1163                px(&value).map(|n| node.text.get_or_insert_with(default_text).size = n)
1164            }
1165            ("weight", Kind::Text) => match num(&value) {
1166                Ok(w) if fonts::WEIGHTS.iter().any(|&k| k as f32 == w) => {
1167                    Ok(node.text.get_or_insert_with(default_text).weight = w as u16)
1168                }
1169                _ => Err("expected 100 to 900 in steps of 100".into()),
1170            },
1171            ("color", Kind::Text) => {
1172                color(&value).map(|c| node.text.get_or_insert_with(default_text).color = c)
1173            }
1174            ("lineHeight", Kind::Text) => {
1175                px(&value).map(|n| node.text.get_or_insert_with(default_text).line_height = n)
1176            }
1177            ("textAlign", Kind::Text) => match str_of(&value).as_deref() {
1178                Some("left") => {
1179                    Ok(node.text.get_or_insert_with(default_text).align = TextAlign::Left)
1180                }
1181                Some("center") => {
1182                    Ok(node.text.get_or_insert_with(default_text).align = TextAlign::Center)
1183                }
1184                Some("right") => {
1185                    Ok(node.text.get_or_insert_with(default_text).align = TextAlign::Right)
1186                }
1187                _ => Err("expected \"left\", \"center\" or \"right\"".into()),
1188            },
1189            ("d", Kind::Vector) => match str_of(&value) {
1190                Some(d) => match path_numbers(&d).map_err(|e| e.to_string()).and_then(|()| {
1191                    kurbo::BezPath::from_svg(&d).map_err(|e| format!("not SVG path data: {e}"))
1192                }) {
1193                    Ok(_) => {
1194                        let view_box = node.vector.as_ref().map(|v| v.view_box);
1195                        let view_box = view_box.unwrap_or_else(|| bounds(&d));
1196                        Ok(node.vector = Some(Vector { d, view_box }))
1197                    }
1198                    Err(e) => Err(e),
1199                },
1200                None => Err("expected SVG path data".into()),
1201            },
1202            ("viewBox", Kind::Vector) => match str_of(&value).as_deref().map(view_box) {
1203                Some(Ok(b)) => {
1204                    let v = node.vector.get_or_insert_with(|| Vector {
1205                        d: String::new(),
1206                        view_box: b,
1207                    });
1208                    Ok(v.view_box = b)
1209                }
1210                Some(Err(e)) => Err(e),
1211                None => Err("expected \"minX minY width height\"".into()),
1212            },
1213            ("src", Kind::Image) => match str_of(&value) {
1214                Some(src) => match join(by, &src) {
1215                    None => {
1216                        let e = format!("src: {src} — outside the served folder");
1217                        self.problem(&at_file, at_pos, e);
1218                        return;
1219                    }
1220                    Some(at) => match self.image_file(&at) {
1221                        None => {
1222                            self.problem(&at_file, at_pos, format!("src: {src} — no such file"));
1223                            return;
1224                        }
1225                        Some(Err(e)) => {
1226                            self.problem(&at_file, at_pos, format!("src: {src} {e}"));
1227                            return;
1228                        }
1229                        Some(Ok(id)) => {
1230                            node.image = Some(Image {
1231                                src: at,
1232                                id,
1233                                at: Origin {
1234                                    file: path.to_string(),
1235                                    pos: attr.pos,
1236                                },
1237                            });
1238                            Ok(())
1239                        }
1240                    },
1241                },
1242                None => Err("expected a path".into()),
1243            },
1244            (name, _) => {
1245                self.problem(
1246                    path,
1247                    attr.pos,
1248                    format!("{} takes no attribute {name}", node.name),
1249                );
1250                return;
1251            }
1252        };
1253        if let Err(e) = ok {
1254            self.problem(path, attr.pos, format!("{}: {e}", attr.name));
1255        }
1256    }
1257
1258    /// The image file at `at`, by its content — read and decoded once per
1259    /// resolve however many `Image`s show it — or why it doesn't decode;
1260    /// `None` when there is no such file.
1261    fn image_file(&mut self, at: &str) -> Option<Result<ImageId, String>> {
1262        self.read_now(at);
1263        if let Some(known) = self.by_path.get(at) {
1264            return Some(known.clone());
1265        }
1266        let file = self.files.image(at)?;
1267        if !self.read.iter().any(|p| p == at) {
1268            self.read.push(at.to_string());
1269        }
1270        let id = file.map(|file| {
1271            let id = file.id;
1272            self.images.entry(id).or_insert(file);
1273            id
1274        });
1275        self.by_path.insert(at.to_string(), id.clone());
1276        Some(id)
1277    }
1278
1279    /// The font file a `Text` written at `pos` in `path` is set in, for its
1280    /// `weight` — `None` for Inter, when it has no `font`. Its `font`, given
1281    /// as `value` by the file `by`, is a name the themes of `by`'s folder
1282    /// give a font. An unknown name is a problem where it is written; a font
1283    /// that can't be used, at its entry in the `themes.yml`, once; a weight
1284    /// the font lacks, at the `weight` — or the `Text`.
1285    fn text_font(
1286        &mut self,
1287        path: &str,
1288        pos: Pos,
1289        font: Option<(&parse::Attr, Value, String)>,
1290        weight: u16,
1291        weight_at: Option<Pos>,
1292    ) -> Option<FontId> {
1293        let Some((attr, value, by)) = font else {
1294            if !fonts::INTER_WEIGHTS.contains(&weight) {
1295                self.no_weight(
1296                    path,
1297                    pos,
1298                    weight_at,
1299                    weight,
1300                    fonts::INTER,
1301                    &fonts::INTER_WEIGHTS,
1302                );
1303            }
1304            return None;
1305        };
1306        let (file, at_pos) = written_at(path, &by, attr, &value);
1307        let Some(name) = str_of(&value) else {
1308            self.problem(
1309                &file,
1310                at_pos,
1311                "font: expected the name of a font the themes name, like \"figtree\"".into(),
1312            );
1313            return None;
1314        };
1315        let folder = by.rsplit_once('/').map_or("", |(folder, _)| folder);
1316        for file in themes_chain(&by) {
1317            self.read_now(&file);
1318        }
1319        let named = self.themes(folder).fonts.clone();
1320        let Some(entry) = named.get(&name) else {
1321            let problem = if named.is_empty() {
1322                format!(
1323                    "font: no font named {name}; no {THEMES_FILE} in this design's folder or above it names fonts"
1324                )
1325            } else {
1326                let names: Vec<&str> = named.keys().map(String::as_str).collect();
1327                format!(
1328                    "font: no font named {name}; the themes name {}",
1329                    names.join(", ")
1330                )
1331            };
1332            self.problem(&file, at_pos, problem);
1333            return None;
1334        };
1335        let origin = entry.origin.clone();
1336        let Some(at) = entry.at.clone() else {
1337            let key = format!("{}\0{}", origin.file, name);
1338            if let std::collections::hash_map::Entry::Vacant(first) = self.families.entry(key) {
1339                first.insert((None, true));
1340                self.problem(
1341                    &origin.file,
1342                    origin.pos,
1343                    format!(
1344                        "fonts.{name}: {} — outside the served folder",
1345                        entry.written
1346                    ),
1347                );
1348            }
1349            return None;
1350        };
1351        self.read_now(&at);
1352        self.read_now(&format!("{at}/"));
1353        let (family, reported) = self.family(&at);
1354        let family = match family {
1355            Some(Ok(family)) => family,
1356            missing_or_bad => {
1357                if !reported {
1358                    let problem = match missing_or_bad {
1359                        Some(Err(e)) => format!("fonts.{name}: {} {e}", entry.written),
1360                        _ => format!("fonts.{name}: {} — no such file or folder", entry.written),
1361                    };
1362                    self.problem(&origin.file, origin.pos, problem);
1363                    if let Some(known) = self.families.get_mut(&at) {
1364                        known.1 = true;
1365                    }
1366                }
1367                return None;
1368            }
1369        };
1370        match family.face(weight) {
1371            Some(face) => {
1372                self.fonts.entry(face.id).or_insert_with(|| face.clone());
1373                Some(face.id)
1374            }
1375            None => {
1376                let has = family.weights();
1377                self.no_weight(path, pos, weight_at, weight, &family.name, &has);
1378                None
1379            }
1380        }
1381    }
1382
1383    /// The problem of a weight the font doesn't have: at the `Text`'s
1384    /// `weight`, or at the `Text` when it leaves the weight at 400.
1385    fn no_weight(
1386        &mut self,
1387        path: &str,
1388        pos: Pos,
1389        weight_at: Option<Pos>,
1390        weight: u16,
1391        family: &str,
1392        has: &[u16],
1393    ) {
1394        let message = if has.is_empty() {
1395            format!(
1396                "weight: {family} has no {weight}; it has no weight from 100 to 900 in steps of 100"
1397            )
1398        } else {
1399            format!(
1400                "weight: {family} has no {weight}; it has {}",
1401                fonts::list(has)
1402            )
1403        };
1404        self.problem(path, weight_at.unwrap_or(pos), message);
1405    }
1406
1407    /// The font at `at` — one file, or a folder of one family's files — read
1408    /// and checked once per resolve, or why it can't be used; `None` when
1409    /// there is nothing there. Also whether its problem was reported yet.
1410    fn family(&mut self, at: &str) -> (Found, bool) {
1411        if let Some(known) = self.families.get(at) {
1412            return known.clone();
1413        }
1414        let family = match self.font_file(at) {
1415            Some(Ok(file)) => Some(Ok(Rc::new(Family {
1416                name: file.family.clone(),
1417                faces: vec![file],
1418            }))),
1419            Some(Err(e)) => Some(Err(e)),
1420            None => self.files.list(at).map(|names| {
1421                let mut faces = Vec::new();
1422                for name in names.into_iter().filter(|n| fonts::is_font_name(n)) {
1423                    match self.font_file(&format!("{at}/{name}")) {
1424                        Some(Ok(file)) => faces.push((name, file)),
1425                        Some(Err(e)) => return Err(format!("holds {name}, which {e}")),
1426                        // Gone since the folder was listed.
1427                        None => {}
1428                    }
1429                }
1430                Family::of_folder(faces).map(Rc::new)
1431            }),
1432        };
1433        self.families
1434            .insert(at.to_string(), (family.clone(), false));
1435        (family, false)
1436    }
1437
1438    /// The font file at `at`, checked, or why it isn't one; `None` when there
1439    /// is no such file.
1440    fn font_file(&mut self, at: &str) -> Option<Result<Arc<FontFile>, String>> {
1441        let file = self.files.font(at)?;
1442        if !self.read.iter().any(|p| p == at) {
1443            self.read.push(at.to_string());
1444        }
1445        Some(file)
1446    }
1447
1448    /// An interaction with its path, if it has one, made relative to the top of
1449    /// the served folder instead of to `path`, the file it is written in.
1450    fn interaction(&mut self, path: &str, action: &str) -> Result<String, String> {
1451        let Some((verb, target)) = action.split_once(' ') else {
1452            return Ok(action.to_string());
1453        };
1454        let target = target.trim();
1455        if !target.ends_with(".design") {
1456            return Err(format!("{action} — a design is a .design file"));
1457        }
1458        match join(path, target) {
1459            None => Err(format!("{action} — outside the served folder")),
1460            Some(at)
1461                if {
1462                    self.read_now(&at);
1463                    self.files.bytes(&at).is_none()
1464                } =>
1465            {
1466                Err(format!("{action} — no such file"))
1467            }
1468            Some(at) => Ok(format!("{verb} {at}")),
1469        }
1470    }
1471
1472    /// Whether `attr`, a `key`, is a string — the one value a parse can
1473    /// match siblings by — saying so where it isn't. True either way once
1474    /// it has been looked at, so an instance doesn't pass it on as a prop.
1475    fn key_ok(&mut self, path: &str, attr: &parse::Attr) -> bool {
1476        if !matches!(attr.value, Value::Str(..)) {
1477            self.problem(path, attr.pos, "key: expected a string".into());
1478        }
1479        true
1480    }
1481
1482    fn common(&mut self, path: &str, by: &str, node: &mut Node, attr: &parse::Attr, value: Value) {
1483        let (at_file, at_pos) = written_at(path, by, attr, &value);
1484        let ok = match attr.name.as_str() {
1485            "x" => num(&value).map(|n| node.x = n),
1486            "y" => num(&value).map(|n| node.y = n),
1487            "width" => size(&value).map(|s| node.width = s),
1488            "height" => size(&value).map(|s| node.height = s),
1489            "name" => str_of(&value)
1490                .map(|s| node.name = s)
1491                .ok_or_else(|| "expected a string".into()),
1492            "opacity" => match value {
1493                Value::Num(n) if (0.0..=1.0).contains(&n) => {
1494                    node.opacity = n as f32;
1495                    Ok(())
1496                }
1497                other => Err(format!(
1498                    "expected a number from 0 to 1, found {}",
1499                    show(&other)
1500                )),
1501            },
1502            "onClick" => match str_of(&value).filter(|s| interaction_ok(s)) {
1503                Some(action) => match self.interaction(by, &action) {
1504                    Ok(a) => {
1505                        node.on_click = Some(a);
1506                        Ok(())
1507                    }
1508                    Err(e) => {
1509                        self.problem(&at_file, at_pos, format!("onClick: {e}"));
1510                        return;
1511                    }
1512                },
1513                None => Err(
1514                    "expected \"navigate <path>\", \"back\", \"overlay <path>\" or \"close\""
1515                        .into(),
1516                ),
1517            },
1518            _ => unreachable!(),
1519        };
1520        if let Err(e) = ok {
1521            self.problem(path, attr.pos, format!("{}: {e}", attr.name));
1522        }
1523    }
1524}
1525
1526fn default_text() -> TextStyle {
1527    TextStyle {
1528        font: None,
1529        content: String::new(),
1530        size: 14.0,
1531        weight: 400,
1532        color: Color::fixed(Rgba {
1533            r: 0x0f,
1534            g: 0x17,
1535            b: 0x2a,
1536            a: 255,
1537        }),
1538        line_height: 1.4,
1539        align: TextAlign::Left,
1540    }
1541}
1542
1543fn view_box(s: &str) -> Result<[f32; 4], String> {
1544    let v: Vec<f32> = s
1545        .split([' ', ','])
1546        .filter(|p| !p.is_empty())
1547        .map(|p| parse::number(p).map(|n| n as f32))
1548        .collect::<Result<_, _>>()?;
1549    match v[..] {
1550        [x, y, w, h] if w > 0.0 && h > 0.0 => Ok([x, y, w, h]),
1551        _ => Err("expected \"minX minY width height\"".into()),
1552    }
1553}
1554
1555/// Every number in SVG path data `d` held to the same limits as a design's
1556/// other numbers. A number here is SVG's — it may have an exponent — and an
1557/// arc's two flags are single digits that may be written run together.
1558fn path_numbers(d: &str) -> Result<(), String> {
1559    let b = d.as_bytes();
1560    let mut i = 0;
1561    let mut command = b' ';
1562    // Which parameter of the current command comes next.
1563    let mut param = 0;
1564    while i < b.len() {
1565        let c = b[i];
1566        if c.is_ascii_alphabetic() && c != b'e' && c != b'E' {
1567            command = c;
1568            param = 0;
1569            i += 1;
1570            continue;
1571        }
1572        if c.is_ascii_whitespace() || c == b',' {
1573            i += 1;
1574            continue;
1575        }
1576        if matches!(command, b'A' | b'a') && matches!(param % 7, 3 | 4) && matches!(c, b'0' | b'1')
1577        {
1578            param += 1;
1579            i += 1;
1580            continue;
1581        }
1582        let start = i;
1583        if matches!(b[i], b'+' | b'-') {
1584            i += 1;
1585        }
1586        let mut dot = false;
1587        while i < b.len() && (b[i].is_ascii_digit() || (b[i] == b'.' && !dot)) {
1588            dot |= b[i] == b'.';
1589            i += 1;
1590        }
1591        if i < b.len() && matches!(b[i], b'e' | b'E') {
1592            let mut j = i + 1;
1593            if j < b.len() && matches!(b[j], b'+' | b'-') {
1594                j += 1;
1595            }
1596            if j < b.len() && b[j].is_ascii_digit() {
1597                i = j;
1598                while i < b.len() && b[i].is_ascii_digit() {
1599                    i += 1;
1600                }
1601            }
1602        }
1603        if i == start {
1604            // Not a number: the path parser says what is wrong.
1605            return Ok(());
1606        }
1607        let s = &d[start..i];
1608        if let Ok(n) = s.parse::<f64>() {
1609            parse::in_range(s, n)?;
1610        }
1611        param += 1;
1612    }
1613    Ok(())
1614}
1615
1616/// A path's own bounds, the view box of a `Vector` that names none.
1617fn bounds(d: &str) -> [f32; 4] {
1618    use kurbo::Shape;
1619    let b = kurbo::BezPath::from_svg(d)
1620        .map(|p| p.bounding_box())
1621        .unwrap_or_default();
1622    [
1623        b.x0 as f32,
1624        b.y0 as f32,
1625        (b.width() as f32).max(1.0),
1626        (b.height() as f32).max(1.0),
1627    ]
1628}
1629
1630fn interaction_ok(s: &str) -> bool {
1631    s == "back"
1632        || s == "close"
1633        || s.strip_prefix("navigate ")
1634            .or(s.strip_prefix("overlay "))
1635            .is_some_and(|p| !p.trim().is_empty())
1636}
1637
1638/// A path relative to a design's file, from the top of the served folder, or
1639/// `None` when it climbs out of the served folder.
1640pub fn join(from: &str, relative: &str) -> Option<String> {
1641    let mut parts: Vec<&str> = from.split('/').filter(|p| !p.is_empty()).collect();
1642    parts.pop();
1643    for part in relative.split('/') {
1644        match part {
1645            "." | "" => {}
1646            ".." => {
1647                parts.pop()?;
1648            }
1649            p => parts.push(p),
1650        }
1651    }
1652    Some(parts.join("/"))
1653}
1654
1655/// `value`, written in `path`, with a prop put in its place, and the file
1656/// that wrote what it ends up as.
1657fn substitute(value: &Value, props: &Props, path: &str) -> (Value, String) {
1658    match value {
1659        Value::Prop(p) => match props.get(p) {
1660            Some(given) => (given.value.clone(), given.by.clone()),
1661            None => (value.clone(), path.to_string()),
1662        },
1663        v => (v.clone(), path.to_string()),
1664    }
1665}
1666
1667/// Where a path in `attr`'s `value` was written: at the attribute, in
1668/// `path`, or — when the value came as a prop from another file, `by` — at
1669/// the string there.
1670fn written_at(path: &str, by: &str, attr: &parse::Attr, value: &Value) -> (String, Pos) {
1671    match value {
1672        Value::Str(_, pos) if by != path => (by.to_string(), *pos),
1673        _ => (path.to_string(), attr.pos),
1674    }
1675}
1676
1677fn show(v: &Value) -> String {
1678    match v {
1679        Value::Str(s, _) => format!("\"{s}\""),
1680        Value::Num(n) => n.to_string(),
1681        Value::Bool(b) => b.to_string(),
1682        Value::Array(_) => "an array".into(),
1683        Value::Object(_) => "an object".into(),
1684        Value::Prop(p) => p.clone(),
1685        Value::Theme(name, _) => format!("theme.{name}"),
1686        Value::Themed { written, .. } => written.clone(),
1687    }
1688}
1689
1690fn str_of(v: &Value) -> Option<String> {
1691    match v {
1692        Value::Str(s, _) => Some(s.clone()),
1693        _ => None,
1694    }
1695}
1696
1697/// Where `key` is written in a themes file's text: at the top level, or —
1698/// with `under` — inside that top-level key's block. YAML's values carry no
1699/// place, so a key is found by its text: on a line of its own, plain or in
1700/// quotes.
1701fn key_pos(text: &str, under: Option<&str>, key: &str) -> Option<Pos> {
1702    let names = |line: &str, key: &str| {
1703        let line = line.trim_start();
1704        [
1705            format!("{key}:"),
1706            format!("\"{key}\":"),
1707            format!("'{key}':"),
1708        ]
1709        .iter()
1710        .any(|k| line.starts_with(k.as_str()))
1711    };
1712    let top = |line: &str| !line.starts_with([' ', '\t']) && !line.trim().is_empty();
1713    let mut inside = under.is_none();
1714    for (n, line) in text.lines().enumerate() {
1715        if line.trim_start().starts_with('#') {
1716            continue;
1717        }
1718        let pos = Pos {
1719            line: n as u32 + 1,
1720            col: (line.len() - line.trim_start().len()) as u32 + 1,
1721        };
1722        match under {
1723            None if top(line) && names(line, key) => return Some(pos),
1724            None => {}
1725            Some(block) if top(line) => inside = names(line, block),
1726            Some(_) if inside && names(line, key) => return Some(pos),
1727            Some(_) => {}
1728        }
1729    }
1730    None
1731}
1732
1733/// The built-in elements; an import can't take one of their names.
1734const ELEMENTS: [&str; 6] = ["Frame", "Text", "Rect", "Ellipse", "Vector", "Image"];
1735
1736fn num(v: &Value) -> Result<f32, String> {
1737    match v {
1738        Value::Num(n) => Ok(*n as f32),
1739        other => Err(format!("expected a number, found {}", show(other))),
1740    }
1741}
1742
1743/// A length or a multiple that can't be negative: pixels, a size, a line height.
1744fn px(v: &Value) -> Result<f32, String> {
1745    match v {
1746        Value::Num(n) if *n >= 0.0 => Ok(*n as f32),
1747        Value::Num(n) => Err(format!("expected a number of 0 or more, found {n}")),
1748        other => Err(format!("expected a number, found {}", show(other))),
1749    }
1750}
1751
1752fn pxs(a: &[Value]) -> Result<Vec<f32>, String> {
1753    a.iter().map(px).collect()
1754}
1755
1756fn size(v: &Value) -> Result<Size, String> {
1757    match v {
1758        Value::Num(n) if *n >= 0.0 => Ok(Size::Px(*n as f32)),
1759        Value::Num(n) => Err(format!("expected pixels of 0 or more, found {n}")),
1760        Value::Str(s, _) if s == "hug" => Ok(Size::Hug),
1761        Value::Str(s, _) if s == "fill" => Ok(Size::Fill),
1762        other => Err(format!(
1763            "expected pixels, \"hug\" or \"fill\", found {}",
1764            show(other)
1765        )),
1766    }
1767}
1768
1769fn align(v: &Value, justify: bool) -> Result<Align, String> {
1770    match str_of(v).as_deref() {
1771        Some("start") => Ok(Align::Start),
1772        Some("center") => Ok(Align::Center),
1773        Some("end") => Ok(Align::End),
1774        Some("space-between") if justify => Ok(Align::SpaceBetween),
1775        _ if justify => Err("expected \"start\", \"center\", \"end\" or \"space-between\"".into()),
1776        _ => Err("expected \"start\", \"center\" or \"end\"".into()),
1777    }
1778}
1779
1780/// A color: a `#…` one, the same in both themes, or a theme color bound to
1781/// its file's themes.
1782fn color(v: &Value) -> Result<Color, String> {
1783    match v {
1784        Value::Str(s, _) => rgba(s).map(Color::fixed),
1785        Value::Themed { light, dark, .. } => Ok(Color {
1786            light: rgba(light)?,
1787            dark: rgba(dark)?,
1788        }),
1789        _ => Err(format!("expected a color, found {}", show(v))),
1790    }
1791}
1792
1793/// A `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa` color.
1794pub fn rgba(s: &str) -> Result<Rgba, String> {
1795    if s.starts_with("theme.") {
1796        return Err(format!(
1797            "{s} is a theme color: write it in braces, {{{s}}}, not in quotes"
1798        ));
1799    }
1800    let hex = s.strip_prefix('#').ok_or("expected a color like #rrggbb")?;
1801    if !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
1802        return Err(format!("{s} is not a color"));
1803    }
1804    let d = |i: usize, n: usize| {
1805        u8::from_str_radix(&hex[i..i + n], 16).map_err(|_| format!("{s} is not a color"))
1806    };
1807    let dd = |i: usize| d(i, 1).map(|v| v * 17);
1808    match hex.len() {
1809        3 => Ok(Rgba {
1810            r: dd(0)?,
1811            g: dd(1)?,
1812            b: dd(2)?,
1813            a: 255,
1814        }),
1815        4 => Ok(Rgba {
1816            r: dd(0)?,
1817            g: dd(1)?,
1818            b: dd(2)?,
1819            a: dd(3)?,
1820        }),
1821        6 => Ok(Rgba {
1822            r: d(0, 2)?,
1823            g: d(2, 2)?,
1824            b: d(4, 2)?,
1825            a: 255,
1826        }),
1827        8 => Ok(Rgba {
1828            r: d(0, 2)?,
1829            g: d(2, 2)?,
1830            b: d(4, 2)?,
1831            a: d(6, 2)?,
1832        }),
1833        _ => Err(format!("{s} is not a color")),
1834    }
1835}
1836
1837/// `c` written as `#rrggbbaa`.
1838fn hex(c: Rgba) -> String {
1839    format!("#{:02x}{:02x}{:02x}{:02x}", c.r, c.g, c.b, c.a)
1840}
1841
1842fn fill(v: &Value) -> Result<Fill, String> {
1843    match v {
1844        Value::Str(s, _) => fill_of(s),
1845        // Each theme's string differs only in its colors, so the two fills
1846        // have the same shape.
1847        Value::Themed { light, dark, .. } => match (fill_of(light)?, fill_of(dark)?) {
1848            (Fill::Solid(l), Fill::Solid(d)) => Ok(Fill::Solid(Color {
1849                light: l.light,
1850                dark: d.light,
1851            })),
1852            (
1853                Fill::Linear { angle, stops: l },
1854                Fill::Linear {
1855                    stops: d,
1856                    angle: dark_angle,
1857                },
1858            ) if angle == dark_angle && l.len() == d.len() => Ok(Fill::Linear {
1859                angle,
1860                stops: l
1861                    .iter()
1862                    .zip(&d)
1863                    .map(|((l, at), (d, _))| {
1864                        (
1865                            Color {
1866                                light: l.light,
1867                                dark: d.light,
1868                            },
1869                            *at,
1870                        )
1871                    })
1872                    .collect(),
1873            }),
1874            _ => Err(format!("{} is not the same fill in both themes", show(v))),
1875        },
1876        _ => Err(format!("expected a color or a gradient, found {}", show(v))),
1877    }
1878}
1879
1880/// A fill written as a string, its colors the same in both themes.
1881fn fill_of(s: &str) -> Result<Fill, String> {
1882    let Some(inner) = s
1883        .strip_prefix("linear-gradient(")
1884        .and_then(|r| r.strip_suffix(')'))
1885    else {
1886        return rgba(s).map(|c| Fill::Solid(Color::fixed(c)));
1887    };
1888    let wrong =
1889        || format!("expected \"linear-gradient(<angle>deg, <color> <stop>%, …)\", found \"{s}\"");
1890    let mut parts = inner.split(',').map(str::trim);
1891    let angle = parts
1892        .next()
1893        .and_then(|a| a.strip_suffix("deg"))
1894        .ok_or_else(wrong)?;
1895    let angle = parse::number(angle.trim())? as f32;
1896    let mut stops = Vec::new();
1897    for part in parts {
1898        let (c, at) = part.split_once(' ').ok_or_else(wrong)?;
1899        let at = at.trim().strip_suffix('%').ok_or_else(wrong)?;
1900        let at = parse::number(at)? as f32;
1901        stops.push((Color::fixed(rgba(c)?), at / 100.0));
1902    }
1903    if stops.len() < 2 {
1904        return Err(wrong());
1905    }
1906    Ok(Fill::Linear { angle, stops })
1907}
1908
1909/// Whether a value holds a theme color, at any depth.
1910fn has_theme(v: &Value) -> bool {
1911    match v {
1912        Value::Themed { .. } | Value::Theme(..) => true,
1913        Value::Array(items) => items.iter().any(has_theme),
1914        Value::Object(fields) => fields.iter().any(|(_, v)| has_theme(v)),
1915        _ => false,
1916    }
1917}
1918
1919fn shadow(v: &Value) -> Result<Shadow, String> {
1920    let Value::Object(fields) = v else {
1921        return Err("expected { x, y, blur, spread, color }".into());
1922    };
1923    let mut s = Shadow {
1924        x: 0.0,
1925        y: 0.0,
1926        blur: 0.0,
1927        spread: 0.0,
1928        color: Color::fixed(Rgba {
1929            r: 0,
1930            g: 0,
1931            b: 0,
1932            a: 64,
1933        }),
1934        at: Origin::default(),
1935    };
1936    for (k, v) in fields {
1937        match k.as_str() {
1938            "x" => s.x = num(v)?,
1939            "y" => s.y = num(v)?,
1940            "blur" => s.blur = num(v)?,
1941            "spread" => s.spread = num(v)?,
1942            "color" => s.color = color(v)?,
1943            other => return Err(format!("a shadow has no {other}")),
1944        }
1945    }
1946    Ok(s)
1947}
1948
1949mod again;
1950
1951pub use again::{Outcome, Resolution};
1952
1953#[cfg(test)]
1954mod tests;