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}