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}