1use 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
26pub enum Theme {
27 #[default]
28 Light,
29 Dark,
30}
31
32impl Theme {
33 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#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct Color {
47 pub light: Rgba,
48 pub dark: Rgba,
49}
50
51impl Color {
52 pub const fn fixed(c: Rgba) -> Color {
54 Color { light: c, dark: c }
55 }
56
57 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#[derive(Debug, Clone, PartialEq)]
100pub enum Fill {
101 Solid(Color),
102 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#[derive(Debug, Clone, PartialEq)]
119pub struct Vector {
120 pub d: String,
121 pub view_box: [f32; 4],
123}
124
125#[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
147pub const MAX_IMAGE_SIDE: u32 = 4096;
152
153#[derive(Debug)]
156pub struct ImageFile {
157 pub id: ImageId,
158 pub bytes: Vec<u8>,
159 pub width: u32,
161 pub height: u32,
162 pub pixels: Arc<Vec<u8>>,
164 pub pixels_width: u32,
165 pub pixels_height: u32,
166}
167
168impl ImageFile {
169 pub fn new(bytes: Vec<u8>) -> Result<ImageFile, String> {
171 ImageFile::with_id(ImageId::of(&bytes), bytes)
172 }
173
174 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 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
204pub 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 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 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#[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#[derive(Debug, Clone, PartialEq)]
299pub struct Image {
300 pub src: String,
301 pub id: ImageId,
302 pub at: Origin,
304}
305
306pub 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 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 pub at: Origin,
372}
373
374#[derive(Debug, Clone, PartialEq)]
375pub struct TextStyle {
376 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#[derive(Debug, Clone, PartialEq)]
389pub struct Node {
390 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 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 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
506pub trait Files {
508 fn bytes(&self, path: &str) -> Option<Vec<u8>>;
510 fn list(&self, path: &str) -> Option<Vec<String>>;
513 fn read(&self, path: &str) -> Option<String> {
515 String::from_utf8(self.bytes(path)?).ok()
516 }
517 fn parse(&self, path: &str, source: &str) -> Result<Arc<parse::File>, Problem> {
522 let _ = path;
523 parse::parse(source).map(Arc::new)
524 }
525 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 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
541pub 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#[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 pub ids: Ids,
576}
577
578impl 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 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 pub fn node(&self, id: &NodeId) -> Option<&Node> {
612 self.at(self.ids.get(id)?)
613 }
614
615 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 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
643pub type ImageFiles = std::collections::BTreeMap<ImageId, Arc<ImageFile>>;
646
647impl PartialEq for ImageFile {
649 fn eq(&self, other: &ImageFile) -> bool {
650 self.id == other.id
651 }
652}
653
654pub fn resolve(files: &dyn Files, path: &str) -> Result<Scene, Vec<Problem>> {
656 resolved(files, path).map(|(scene, _)| scene)
657}
658
659fn 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 missing: Vec<String>,
709 depth: usize,
711 images: ImageFiles,
713 by_path: HashMap<String, Result<ImageId, String>>,
715 fonts: FontFiles,
717 families: HashMap<String, (Found, bool)>,
721 themes: HashMap<String, Rc<Themes>>,
724 themed: bool,
726 instance: Vec<u32>,
729 record: Record,
731 current: Option<NodeId>,
734 own_only: bool,
737}
738
739#[derive(Debug, Clone, Default)]
742struct Record {
743 instances: BTreeMap<Vec<u32>, Instance>,
746 by_file: HashMap<String, BTreeSet<Vec<u32>>>,
748 readers: HashMap<String, BTreeSet<Vec<u32>>>,
751 reads: BTreeMap<Vec<u32>, BTreeSet<String>>,
753 themed: BTreeSet<NodeId>,
755 parses: HashMap<String, Arc<parse::File>>,
757}
758
759impl Record {
760 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 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#[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#[derive(Debug, Clone)]
803struct Caller {
804 file: String,
805 overrides: Vec<(parse::Attr, String)>,
806}
807
808fn 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
819fn 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#[derive(Debug, Clone, Default)]
836struct Themes {
837 found: bool,
839 light: BTreeMap<String, Rgba>,
840 dark: BTreeMap<String, Rgba>,
841 fonts: BTreeMap<String, ThemesFont>,
843}
844
845#[derive(Debug, Clone)]
848struct ThemesFont {
849 written: String,
850 at: Option<String>,
851 origin: Origin,
852}
853
854struct ThemesFile {
857 light: BTreeMap<String, Rgba>,
858 dark: BTreeMap<String, Rgba>,
859 fonts: BTreeMap<String, ThemesFont>,
860}
861
862pub const THEMES_FILE: &str = "themes.yml";
864
865const MAX_DEPTH: usize = parse::MAX_DEPTH;
868
869#[derive(Debug, Clone)]
872struct Given {
873 value: Value,
874 by: String,
875}
876
877type Props = HashMap<String, Given>;
878
879type Found = Option<Result<Rc<Family>, String>>;
882
883impl Resolver<'_> {
884 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 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 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 fn bind(&mut self, path: &str, value: &mut Value) {
932 match value {
933 Value::Theme(name, pos) => {
934 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 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 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 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 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 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 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 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 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 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 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 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 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 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 "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 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 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 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 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 #[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 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 ("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 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 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 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 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 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 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 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 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
2175fn path_numbers(d: &str) -> Result<(), String> {
2179 let b = d.as_bytes();
2180 let mut i = 0;
2181 let mut command = b' ';
2182 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 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
2236fn 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
2258pub 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
2275fn 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
2287fn 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
2317fn 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
2353const 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
2363fn 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
2400fn 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
2413pub 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
2457fn 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 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
2500fn 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
2529fn 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;