//! Syntax highlighter for Storybook DSL //! //! Implements iced's Highlighter trait to provide line-by-line syntax //! highlighting. use std::ops::Range; use iced::{ Color, advanced::text::highlighter, }; use crate::syntax_highlight::{ token_to_color_contextual, tokenize, }; /// Settings for the Storybook highlighter #[derive(Debug, Clone, PartialEq)] pub struct Settings; /// Storybook syntax highlighter #[derive(Debug)] pub struct StorybookHighlighter { current_line: usize, in_prose_block: bool, // Track if we're inside a prose block } impl iced::widget::text::Highlighter for StorybookHighlighter { type Highlight = Color; type Iterator<'a> = std::vec::IntoIter<(Range, Self::Highlight)>; type Settings = Settings; fn new(_settings: &Self::Settings) -> Self { Self { current_line: 0, in_prose_block: false, } } fn update(&mut self, _new_settings: &Self::Settings) { // No settings to update } fn change_line(&mut self, line: usize) { self.current_line = line; // Reset prose block state when jumping to a different line self.in_prose_block = false; } fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { let mut highlights = Vec::new(); // Check if this line starts or ends a prose block let trimmed = line.trim_start(); if trimmed.starts_with("---") { // This is a prose marker line if self.in_prose_block { // Ending a prose block self.in_prose_block = false; } else { // Starting a prose block self.in_prose_block = true; } // Color the entire line as a prose marker (gray) if !line.is_empty() { highlights.push((0..line.len(), Color::from_rgb8(0x6d, 0x6d, 0x6d))); } } else if self.in_prose_block { // Inside a prose block - render as plain text in peach color if !line.is_empty() { highlights.push((0..line.len(), Color::from_rgb8(0xff, 0xb8, 0x6c))); } } else { // Regular code - tokenize and highlight let tokens = tokenize(line); // Track if we're after a colon for context-aware coloring let mut after_colon = false; for (token, range) in tokens { let color = token_to_color_contextual(&token, after_colon); highlights.push((range, color)); // Update context: set after_colon when we see a colon, // reset it when we see an identifier (field value) or certain other tokens use storybook::syntax::lexer::Token; match &token { | Token::Colon => after_colon = true, | Token::Ident(_) | Token::IntLit(_) | Token::FloatLit(_) | Token::StringLit(_) | Token::True | Token::False | Token::TimeLit(_) | Token::DurationLit(_) => { // Reset after consuming a value after_colon = false; }, // Don't reset for whitespace, commas, or other punctuation | _ => {}, } } } self.current_line += 1; highlights.into_iter() } fn current_line(&self) -> usize { self.current_line } } /// Convert a highlight (Color) to a Format for rendering pub fn to_format(color: &Color, _theme: &iced::Theme) -> highlighter::Format { highlighter::Format { color: Some(*color), font: None, } }