Skip to main content

standout_render/style/
file_registry.rs

1//! Stylesheet registry for file-based theme loading.
2//!
3//! This module provides [`StylesheetRegistry`], which manages theme resolution
4//! from multiple sources: inline YAML, filesystem directories, or embedded content.
5//!
6//! # Design
7//!
8//! The registry is a thin wrapper around [`FileRegistry<Theme>`](crate::file_loader::FileRegistry),
9//! providing stylesheet-specific functionality while reusing the generic file loading infrastructure.
10//!
11//! The registry uses a two-phase approach:
12//!
13//! 1. Collection: Stylesheets are collected from various sources (inline, directories, embedded)
14//! 2. Resolution: A unified map resolves theme names to their parsed `Theme` instances
15//!
16//! This separation enables:
17//! - Testability: Resolution logic can be tested without filesystem access
18//! - Flexibility: Same resolution rules apply regardless of stylesheet source
19//! - Hot reloading: Files are re-read and re-parsed on each access in development mode
20//!
21//! # Stylesheet Resolution
22//!
23//! Stylesheets are resolved by name using these rules:
24//!
25//! 1. Inline stylesheets (added via [`StylesheetRegistry::add_inline`]) have highest priority
26//! 2. File stylesheets are searched in directory registration order (first directory wins)
27//! 3. Names can be specified with or without extension: both `"darcula"` and `"darcula.yaml"` resolve
28//!
29//! # Supported Extensions
30//!
31//! Stylesheet files are recognized by extension, in priority order:
32//!
33//! | Priority | Extension | Description |
34//! |----------|-----------|-------------|
35//! | 1 (highest) | `.yaml` | Standard YAML extension |
36//! | 2 (lowest) | `.yml` | Short YAML extension |
37//!
38//! If multiple files exist with the same base name but different extensions
39//! (e.g., `darcula.yaml` and `darcula.yml`), the higher-priority extension wins.
40//!
41//! # Collision Handling
42//!
43//! The registry enforces strict collision rules:
44//!
45//! - Same-directory, different extensions: Higher priority extension wins (no error)
46//! - Cross-directory collisions: Panic with detailed message listing conflicting files
47//!
48//! # Example
49//!
50//! ```rust,ignore
51//! use standout_render::style::StylesheetRegistry;
52//!
53//! let mut registry = StylesheetRegistry::new();
54//! registry.add_dir("./themes")?;
55//!
56//! // Get a theme by name
57//! let theme = registry.get("darcula")?;
58//! ```
59
60use std::collections::HashMap;
61use std::path::Path;
62
63use super::super::theme::Theme;
64use crate::file_loader::{
65    build_embedded_registry, resolve_in_map, FileRegistry, FileRegistryConfig, LoadError,
66};
67
68use super::error::StylesheetError;
69
70/// Recognized stylesheet file extensions in priority order.
71///
72/// When multiple files exist with the same base name but different extensions,
73/// the extension appearing earlier in this list takes precedence.
74pub const STYLESHEET_EXTENSIONS: &[&str] = &[".css", ".yaml", ".yml"];
75
76/// Creates the file registry configuration for stylesheets.
77fn stylesheet_config() -> FileRegistryConfig<Theme> {
78    FileRegistryConfig {
79        extensions: STYLESHEET_EXTENSIONS,
80        transform: |content| {
81            parse_theme_content(content).map_err(|e| LoadError::Transform {
82                name: String::new(), // FileRegistry fills in the actual name
83                message: e.to_string(),
84            })
85        },
86    }
87}
88
89/// Parses theme content, auto-detecting CSS vs YAML format.
90///
91/// CSS is detected by the presence of a CSS class selector (`.name {`),
92/// which distinguishes it from YAML inline maps that also use `{`.
93fn parse_theme_content(content: &str) -> Result<Theme, StylesheetError> {
94    let trimmed = content.trim_start();
95    // CSS files start with class selectors (.name), comments (/*), or @media queries
96    if trimmed.starts_with('.') || trimmed.starts_with("/*") || trimmed.starts_with("@media") {
97        Theme::from_css(content)
98    } else {
99        Theme::from_yaml(content)
100    }
101}
102
103/// Registry for stylesheet/theme resolution from multiple sources.
104///
105/// The registry maintains a unified view of themes from:
106/// - Inline YAML strings (highest priority)
107/// - Multiple filesystem directories
108/// - Embedded content (for release builds)
109///
110/// # Resolution Order
111///
112/// When looking up a theme name:
113///
114/// 1. Check inline themes first
115/// 2. Check file-based themes in registration order
116/// 3. Return error if not found
117///
118/// # Hot Reloading
119///
120/// In development mode (debug builds), file-based themes are re-read and
121/// re-parsed on each access, enabling rapid iteration without restarts.
122///
123/// # Example
124///
125/// ```rust,ignore
126/// let mut registry = StylesheetRegistry::new();
127///
128/// // Add inline theme (highest priority)
129/// registry.add_inline("custom", r#"
130/// header:
131///   fg: cyan
132///   bold: true
133/// "#)?;
134///
135/// // Add from directory
136/// registry.add_dir("./themes")?;
137///
138/// // Get a theme
139/// let theme = registry.get("darcula")?;
140/// ```
141pub struct StylesheetRegistry {
142    /// The underlying file registry for directory-based file loading.
143    inner: FileRegistry<Theme>,
144
145    /// Inline themes (stored separately for highest priority).
146    inline: HashMap<String, Theme>,
147}
148
149impl Default for StylesheetRegistry {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155impl StylesheetRegistry {
156    /// Creates an empty stylesheet registry.
157    pub fn new() -> Self {
158        Self {
159            inner: FileRegistry::new(stylesheet_config()),
160            inline: HashMap::new(),
161        }
162    }
163
164    /// Adds an inline theme from a YAML string.
165    ///
166    /// Inline themes have the highest priority and will shadow any
167    /// file-based themes with the same name.
168    ///
169    /// # Arguments
170    ///
171    /// * `name` - The theme name for resolution
172    /// * `yaml` - The YAML content defining the theme
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if the YAML content cannot be parsed.
177    ///
178    /// # Example
179    ///
180    /// ```rust,ignore
181    /// registry.add_inline("custom", r#"
182    /// header:
183    ///   fg: cyan
184    ///   bold: true
185    /// muted:
186    ///   dim: true
187    /// "#)?;
188    /// ```
189    pub fn add_inline(
190        &mut self,
191        name: impl Into<String>,
192        yaml: &str,
193    ) -> Result<(), StylesheetError> {
194        let theme = Theme::from_yaml(yaml)?;
195        self.inline.insert(name.into(), theme);
196        Ok(())
197    }
198
199    /// Adds a pre-parsed theme directly.
200    ///
201    /// This is useful when you have a `Theme` instance already constructed
202    /// programmatically and want to register it in the registry.
203    ///
204    /// # Arguments
205    ///
206    /// * `name` - The theme name for resolution
207    /// * `theme` - The pre-built theme instance
208    pub fn add_theme(&mut self, name: impl Into<String>, theme: Theme) {
209        self.inline.insert(name.into(), theme);
210    }
211
212    /// Adds a stylesheet directory to search for files.
213    ///
214    /// Themes in the directory are resolved by their filename without
215    /// extension. For example, with directory `./themes`:
216    ///
217    /// - `"darcula"` → `./themes/darcula.yaml`
218    /// - `"monokai"` → `./themes/monokai.yaml`
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if the directory doesn't exist.
223    ///
224    /// # Example
225    ///
226    /// ```rust,ignore
227    /// registry.add_dir("./themes")?;
228    /// let theme = registry.get("darcula")?;
229    /// ```
230    pub fn add_dir<P: AsRef<Path>>(&mut self, path: P) -> Result<(), StylesheetError> {
231        self.inner.add_dir(path).map_err(|e| StylesheetError::Load {
232            message: e.to_string(),
233        })
234    }
235
236    /// Adds pre-embedded themes (for release builds).
237    ///
238    /// Embedded themes are stored directly in memory without filesystem access.
239    /// This is typically used with `include_str!` to bundle themes at compile time.
240    ///
241    /// # Arguments
242    ///
243    /// * `themes` - Map of theme name to parsed Theme
244    pub fn add_embedded(&mut self, themes: HashMap<String, Theme>) {
245        for (name, theme) in themes {
246            self.inline.insert(name, theme);
247        }
248    }
249
250    /// Adds a pre-embedded theme by name.
251    ///
252    /// This is a convenience method for adding a single embedded theme.
253    ///
254    /// # Arguments
255    ///
256    /// * `name` - The theme name for resolution
257    /// * `theme` - The pre-built theme instance
258    pub fn add_embedded_theme(&mut self, name: impl Into<String>, theme: Theme) {
259        self.inner.add_embedded(&name.into(), theme);
260    }
261
262    /// Creates a registry from embedded stylesheet entries.
263    ///
264    /// This is the primary entry point for compile-time embedded stylesheets,
265    /// typically called by the `embed_styles!` macro.
266    ///
267    /// # Arguments
268    ///
269    /// * `entries` - Slice of `(name_with_ext, yaml_content)` pairs where `name_with_ext`
270    ///   is the relative path including extension (e.g., `"themes/dark.yaml"`)
271    ///
272    /// # Processing
273    ///
274    /// This method applies the same logic as runtime file loading:
275    ///
276    /// 1. YAML parsing: Each entry's content is parsed as a theme definition
277    /// 2. Extension stripping: `"themes/dark.yaml"` → `"themes/dark"`
278    /// 3. Extension priority: When multiple files share a base name, the
279    ///    higher-priority extension wins (see [`STYLESHEET_EXTENSIONS`])
280    /// 4. Dual registration: Each theme is accessible by both its base
281    ///    name and its full name with extension
282    ///
283    /// # Errors
284    ///
285    /// Returns an error if any YAML content fails to parse.
286    ///
287    /// # Example
288    ///
289    /// ```rust
290    /// use standout_render::style::StylesheetRegistry;
291    ///
292    /// // Typically generated by embed_styles! macro
293    /// let entries: &[(&str, &str)] = &[
294    ///     ("default.yaml", "header:\n  fg: cyan\n  bold: true"),
295    ///     ("themes/dark.yaml", "panel:\n  fg: white"),
296    /// ];
297    ///
298    /// let mut registry = StylesheetRegistry::from_embedded_entries(entries).unwrap();
299    ///
300    /// // Access by base name or full name
301    /// assert!(registry.get("default").is_ok());
302    /// assert!(registry.get("default.yaml").is_ok());
303    /// assert!(registry.get("themes/dark").is_ok());
304    /// ```
305    pub fn from_embedded_entries(entries: &[(&str, &str)]) -> Result<Self, StylesheetError> {
306        let mut registry = Self::new();
307
308        // Use shared helper with auto-detecting CSS/YAML parsing
309        registry.inline = build_embedded_registry(entries, STYLESHEET_EXTENSIONS, |content| {
310            parse_theme_content(content)
311        })?;
312
313        Ok(registry)
314    }
315
316    /// Gets a theme by name.
317    ///
318    /// Names are resolved with extension-agnostic fallback: if the exact name
319    /// isn't found and it has a recognized extension, the extension is stripped
320    /// and the base name is tried. This allows lookups like `"config.yml"` to
321    /// find a theme registered as `"config"` (from `config.yaml`).
322    ///
323    /// Looks up the theme in order: inline first, then file-based.
324    /// In development mode, file-based themes are re-read on each access.
325    ///
326    /// # Arguments
327    ///
328    /// * `name` - The theme name (with or without extension)
329    ///
330    /// # Errors
331    ///
332    /// Returns an error if the theme is not found or cannot be parsed.
333    ///
334    /// # Example
335    ///
336    /// ```rust,ignore
337    /// let theme = registry.get("darcula")?;
338    /// ```
339    pub fn get(&mut self, name: &str) -> Result<Theme, StylesheetError> {
340        // Check inline first (with extension-agnostic fallback)
341        if let Some(theme) = resolve_in_map(&self.inline, name, STYLESHEET_EXTENSIONS) {
342            return Ok(theme.clone());
343        }
344
345        // Try file-based (FileRegistry has its own extension fallback)
346        let theme = self.inner.get(name).map_err(|e| StylesheetError::Load {
347            message: e.to_string(),
348        })?;
349
350        // Set the theme name from the lookup key (strip extension if present)
351        let base_name = crate::file_loader::strip_extension(name, STYLESHEET_EXTENSIONS);
352        Ok(theme.with_name(base_name))
353    }
354
355    /// Checks if a theme exists in the registry.
356    ///
357    /// # Arguments
358    ///
359    /// * `name` - The theme name to check
360    pub fn contains(&self, name: &str) -> bool {
361        resolve_in_map(&self.inline, name, STYLESHEET_EXTENSIONS).is_some()
362            || self.inner.get_entry(name).is_some()
363    }
364
365    /// Returns an iterator over all registered theme names.
366    pub fn names(&self) -> impl Iterator<Item = &str> {
367        self.inline
368            .keys()
369            .map(|s| s.as_str())
370            .chain(self.inner.names())
371    }
372
373    /// Returns the number of registered themes.
374    pub fn len(&self) -> usize {
375        self.inline.len() + self.inner.len()
376    }
377
378    /// Returns true if no themes are registered.
379    pub fn is_empty(&self) -> bool {
380        self.inline.is_empty() && self.inner.is_empty()
381    }
382
383    /// Clears all registered themes.
384    pub fn clear(&mut self) {
385        self.inline.clear();
386        self.inner.clear();
387    }
388
389    /// Refreshes file-based themes from disk.
390    ///
391    /// This re-walks all registered directories and updates the internal
392    /// cache. Useful in long-running applications that need to pick up
393    /// theme changes without restarting.
394    ///
395    /// # Errors
396    ///
397    /// Returns an error if any directory cannot be read.
398    pub fn refresh(&mut self) -> Result<(), StylesheetError> {
399        self.inner.refresh().map_err(|e| StylesheetError::Load {
400            message: e.to_string(),
401        })
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use std::fs;
409    use tempfile::TempDir;
410
411    #[test]
412    fn test_registry_new_is_empty() {
413        let registry = StylesheetRegistry::new();
414        assert!(registry.is_empty());
415        assert_eq!(registry.len(), 0);
416    }
417
418    #[test]
419    fn test_registry_add_inline() {
420        let mut registry = StylesheetRegistry::new();
421        registry
422            .add_inline(
423                "test",
424                r#"
425                header:
426                    fg: cyan
427                    bold: true
428                "#,
429            )
430            .unwrap();
431
432        assert!(!registry.is_empty());
433        assert_eq!(registry.len(), 1);
434        assert!(registry.contains("test"));
435    }
436
437    #[test]
438    fn test_registry_add_theme() {
439        let mut registry = StylesheetRegistry::new();
440        let theme = Theme::new().add("header", console::Style::new().cyan().bold());
441        registry.add_theme("custom", theme);
442
443        assert!(registry.contains("custom"));
444        let retrieved = registry.get("custom").unwrap();
445        assert!(retrieved.resolve_styles(None).has("header"));
446    }
447
448    #[test]
449    fn test_registry_get_inline() {
450        let mut registry = StylesheetRegistry::new();
451        registry
452            .add_inline(
453                "darcula",
454                r#"
455                header:
456                    fg: cyan
457                muted:
458                    dim: true
459                "#,
460            )
461            .unwrap();
462
463        let theme = registry.get("darcula").unwrap();
464        let styles = theme.resolve_styles(None);
465        assert!(styles.has("header"));
466        assert!(styles.has("muted"));
467    }
468
469    #[test]
470    fn test_registry_add_dir() {
471        let temp_dir = TempDir::new().unwrap();
472        let theme_path = temp_dir.path().join("monokai.yaml");
473        fs::write(
474            &theme_path,
475            r#"
476            keyword:
477                fg: magenta
478                bold: true
479            string:
480                fg: green
481            "#,
482        )
483        .unwrap();
484
485        let mut registry = StylesheetRegistry::new();
486        registry.add_dir(temp_dir.path()).unwrap();
487
488        let theme = registry.get("monokai").unwrap();
489        let styles = theme.resolve_styles(None);
490        assert!(styles.has("keyword"));
491        assert!(styles.has("string"));
492    }
493
494    #[test]
495    fn test_registry_inline_shadows_file() {
496        let temp_dir = TempDir::new().unwrap();
497        let theme_path = temp_dir.path().join("test.yaml");
498        fs::write(
499            &theme_path,
500            r#"
501            from_file:
502                fg: red
503            header:
504                fg: red
505            "#,
506        )
507        .unwrap();
508
509        let mut registry = StylesheetRegistry::new();
510        registry.add_dir(temp_dir.path()).unwrap();
511        registry
512            .add_inline(
513                "test",
514                r#"
515            from_inline:
516                fg: blue
517            header:
518                fg: blue
519            "#,
520            )
521            .unwrap();
522
523        // Inline should win
524        let theme = registry.get("test").unwrap();
525        let styles = theme.resolve_styles(None);
526        assert!(styles.has("from_inline"));
527        assert!(!styles.has("from_file"));
528    }
529
530    #[test]
531    fn test_registry_extension_priority() {
532        let temp_dir = TempDir::new().unwrap();
533
534        // Create both .yaml and .yml with different content
535        fs::write(
536            temp_dir.path().join("theme.yaml"),
537            r#"
538            from_yaml:
539                fg: cyan
540            source:
541                fg: cyan
542            "#,
543        )
544        .unwrap();
545
546        fs::write(
547            temp_dir.path().join("theme.yml"),
548            r#"
549            from_yml:
550                fg: red
551            source:
552                fg: red
553            "#,
554        )
555        .unwrap();
556
557        let mut registry = StylesheetRegistry::new();
558        registry.add_dir(temp_dir.path()).unwrap();
559
560        // .yaml should win over .yml
561        let theme = registry.get("theme").unwrap();
562        let styles = theme.resolve_styles(None);
563        assert!(styles.has("from_yaml"));
564        assert!(!styles.has("from_yml"));
565    }
566
567    #[test]
568    fn test_registry_names() {
569        let mut registry = StylesheetRegistry::new();
570        registry.add_inline("alpha", "header: bold").unwrap();
571        registry.add_inline("beta", "header: dim").unwrap();
572
573        let names: Vec<&str> = registry.names().collect();
574        assert!(names.contains(&"alpha"));
575        assert!(names.contains(&"beta"));
576    }
577
578    #[test]
579    fn test_registry_clear() {
580        let mut registry = StylesheetRegistry::new();
581        registry.add_inline("test", "header: bold").unwrap();
582        assert!(!registry.is_empty());
583
584        registry.clear();
585        assert!(registry.is_empty());
586    }
587
588    #[test]
589    fn test_registry_not_found() {
590        let mut registry = StylesheetRegistry::new();
591        let result = registry.get("nonexistent");
592        assert!(result.is_err());
593    }
594
595    #[test]
596    fn test_registry_invalid_yaml() {
597        let mut registry = StylesheetRegistry::new();
598        let result = registry.add_inline("bad", "not: [valid: yaml");
599        assert!(result.is_err());
600    }
601
602    #[test]
603    fn test_registry_hot_reload() {
604        let temp_dir = TempDir::new().unwrap();
605        let theme_path = temp_dir.path().join("dynamic.yaml");
606        fs::write(
607            &theme_path,
608            r#"
609            version_v1:
610                fg: red
611            header:
612                fg: red
613            "#,
614        )
615        .unwrap();
616
617        let mut registry = StylesheetRegistry::new();
618        registry.add_dir(temp_dir.path()).unwrap();
619
620        // First read
621        let theme1 = registry.get("dynamic").unwrap();
622        let styles1 = theme1.resolve_styles(None);
623        assert!(styles1.has("version_v1"));
624
625        // Update the file
626        fs::write(
627            &theme_path,
628            r#"
629            version_v2:
630                fg: green
631            updated_style:
632                fg: blue
633            header:
634                fg: blue
635            "#,
636        )
637        .unwrap();
638
639        // Refresh and read again
640        registry.refresh().unwrap();
641        let theme2 = registry.get("dynamic").unwrap();
642        let styles2 = theme2.resolve_styles(None);
643        assert!(styles2.has("updated_style"));
644    }
645
646    #[test]
647    fn test_registry_adaptive_theme() {
648        let mut registry = StylesheetRegistry::new();
649        registry
650            .add_inline(
651                "adaptive",
652                r#"
653            panel:
654                fg: gray
655                light:
656                    fg: black
657                dark:
658                    fg: white
659            "#,
660            )
661            .unwrap();
662
663        let theme = registry.get("adaptive").unwrap();
664
665        // Check light mode
666        let light_styles = theme.resolve_styles(Some(crate::ColorMode::Light));
667        assert!(light_styles.has("panel"));
668
669        // Check dark mode
670        let dark_styles = theme.resolve_styles(Some(crate::ColorMode::Dark));
671        assert!(dark_styles.has("panel"));
672    }
673
674    // =========================================================================
675    // from_embedded_entries tests
676    // =========================================================================
677
678    #[test]
679    fn test_from_embedded_entries_single() {
680        let entries: &[(&str, &str)] = &[("test.yaml", "header:\n    fg: cyan\n    bold: true")];
681        let registry = StylesheetRegistry::from_embedded_entries(entries).unwrap();
682
683        // Should be accessible by both names
684        assert!(registry.contains("test"));
685        assert!(registry.contains("test.yaml"));
686    }
687
688    #[test]
689    fn test_from_embedded_entries_multiple() {
690        let entries: &[(&str, &str)] = &[
691            ("light.yaml", "header:\n    fg: black"),
692            ("dark.yaml", "header:\n    fg: white"),
693        ];
694        let registry = StylesheetRegistry::from_embedded_entries(entries).unwrap();
695
696        assert_eq!(registry.len(), 4); // 2 base + 2 with ext
697        assert!(registry.contains("light"));
698        assert!(registry.contains("dark"));
699    }
700
701    #[test]
702    fn test_from_embedded_entries_nested_paths() {
703        let entries: &[(&str, &str)] = &[
704            ("themes/monokai.yaml", "keyword:\n    fg: magenta"),
705            ("themes/solarized.yaml", "keyword:\n    fg: cyan"),
706        ];
707        let registry = StylesheetRegistry::from_embedded_entries(entries).unwrap();
708
709        assert!(registry.contains("themes/monokai"));
710        assert!(registry.contains("themes/monokai.yaml"));
711        assert!(registry.contains("themes/solarized"));
712    }
713
714    #[test]
715    fn test_from_embedded_entries_extension_priority() {
716        // .yaml has higher priority than .yml (index 0 vs index 1)
717        let entries: &[(&str, &str)] = &[
718            ("config.yml", "from_yml:\n    fg: red"),
719            ("config.yaml", "from_yaml:\n    fg: cyan"),
720        ];
721        let mut registry = StylesheetRegistry::from_embedded_entries(entries).unwrap();
722
723        // Base name should resolve to higher priority (.yaml)
724        let theme = registry.get("config").unwrap();
725        let styles = theme.resolve_styles(None);
726        assert!(styles.has("from_yaml"));
727        assert!(!styles.has("from_yml"));
728
729        // Both can still be accessed by full name
730        let yml_theme = registry.get("config.yml").unwrap();
731        assert!(yml_theme.resolve_styles(None).has("from_yml"));
732    }
733
734    #[test]
735    fn test_from_embedded_entries_extension_priority_reverse_order() {
736        // Same test but with entries in reverse order to ensure sorting works
737        let entries: &[(&str, &str)] = &[
738            ("config.yaml", "from_yaml:\n    fg: cyan"),
739            ("config.yml", "from_yml:\n    fg: red"),
740        ];
741        let mut registry = StylesheetRegistry::from_embedded_entries(entries).unwrap();
742
743        // Base name should still resolve to higher priority (.yaml)
744        let theme = registry.get("config").unwrap();
745        let styles = theme.resolve_styles(None);
746        assert!(styles.has("from_yaml"));
747    }
748
749    #[test]
750    fn test_from_embedded_entries_names_iterator() {
751        let entries: &[(&str, &str)] =
752            &[("a.yaml", "header: bold"), ("nested/b.yaml", "header: dim")];
753        let registry = StylesheetRegistry::from_embedded_entries(entries).unwrap();
754
755        let names: Vec<&str> = registry.names().collect();
756        assert!(names.contains(&"a"));
757        assert!(names.contains(&"a.yaml"));
758        assert!(names.contains(&"nested/b"));
759        assert!(names.contains(&"nested/b.yaml"));
760    }
761
762    #[test]
763    fn test_from_embedded_entries_empty() {
764        let entries: &[(&str, &str)] = &[];
765        let registry = StylesheetRegistry::from_embedded_entries(entries).unwrap();
766
767        assert!(registry.is_empty());
768        assert_eq!(registry.len(), 0);
769    }
770
771    #[test]
772    fn test_from_embedded_entries_invalid_yaml() {
773        let entries: &[(&str, &str)] = &[("bad.yaml", "not: [valid: yaml")];
774        let result = StylesheetRegistry::from_embedded_entries(entries);
775
776        assert!(result.is_err());
777    }
778}