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