Skip to main content

oak_tailwind/parser/
mod.rs

1//! Parser implementation for Tailwind DSL.
2/// Element types for the Tailwind language.
3pub mod element_type;
4
5use crate::{
6    ast::{TailwindArbitraryValue, TailwindClass, TailwindClassKind, TailwindComment, TailwindDirective, TailwindModifier, TailwindNode, TailwindRoot, TailwindUtility},
7    language::TailwindLanguage,
8    lexer::{TailwindLexer, token_type::TailwindTokenType},
9    parser::element_type::TailwindElementType,
10};
11use core::range::Range;
12use oak_core::{
13    errors::OakError,
14    parser::{ParseCache, ParseOutput, Parser, ParserState, parse_with_lexer},
15    source::{Source, TextEdit},
16};
17
18pub(crate) type State<'a, S> = ParserState<'a, TailwindLanguage, S>;
19
20/// Parser for the Tailwind language.
21#[derive(Debug, Clone, Copy, Default)]
22pub struct TailwindParser {
23    /// Language configuration
24    pub config: TailwindLanguage,
25}
26
27impl TailwindParser {
28    /// Creates a new `TailwindParser` with the given configuration.
29    pub fn new(config: TailwindLanguage) -> Self {
30        Self { config }
31    }
32
33    fn parse_node<'a, S: Source + ?Sized>(&self, state: &mut State<'a, S>) -> Result<Option<TailwindNode>, OakError> {
34        match state.peek_kind() {
35            Some(TailwindTokenType::Directive) => {
36                let directive = self.parse_directive(state)?;
37                Ok(Some(TailwindNode::Directive(directive)))
38            }
39            Some(TailwindTokenType::Modifier) | Some(TailwindTokenType::Utility) | Some(TailwindTokenType::Important) | Some(TailwindTokenType::ArbitraryValue) => {
40                let class = self.parse_class(state)?;
41                Ok(Some(TailwindNode::Class(class)))
42            }
43            Some(TailwindTokenType::Comment) => {
44                let token = state.current().unwrap();
45                let start_pos = token.span.start;
46                let content = state.source.get_text_in(token.span).to_string();
47
48                let cp = state.checkpoint();
49                state.bump();
50                state.finish_at(cp, TailwindElementType::Comment);
51
52                let end_pos = state.current_offset();
53                Ok(Some(TailwindNode::Comment(TailwindComment { span: Range { start: start_pos, end: end_pos }, content })))
54            }
55            _ => {
56                state.advance();
57                Ok(None)
58            }
59        }
60    }
61
62    /// Parses a Tailwind class (e.g., hover:bg-red-500, !p-4).
63    fn parse_class<'a, S: Source + ?Sized>(&self, state: &mut State<'a, S>) -> Result<TailwindClass, OakError> {
64        let checkpoint = state.checkpoint();
65        let start_pos = state.current_offset();
66        let mut is_important = false;
67
68        // Optional important flag at start
69        if state.eat(TailwindTokenType::Important) {
70            is_important = true;
71        }
72
73        let mut modifiers = Vec::new();
74        // Zero or more modifiers
75        while state.at(TailwindTokenType::Modifier) {
76            let mod_token = state.current().unwrap();
77            let mod_range = mod_token.span;
78            let mod_text = state.source.get_text_in(mod_range).to_string();
79
80            let mod_cp = state.checkpoint();
81            state.bump();
82            state.finish_at(mod_cp, TailwindElementType::Modifier);
83
84            modifiers.push(TailwindModifier { span: mod_range, name: mod_text });
85        }
86
87        // Utility or arbitrary value
88        let kind = if state.at(TailwindTokenType::Utility) {
89            let util_token = state.current().unwrap();
90            let util_range = util_token.span;
91            let util_text = state.source.get_text_in(util_range).to_string();
92
93            let util_cp = state.checkpoint();
94            state.bump();
95            state.finish_at(util_cp, TailwindElementType::Utility);
96
97            TailwindClassKind::Utility(TailwindUtility { span: util_range, name: util_text })
98        }
99        else if state.at(TailwindTokenType::ArbitraryValue) {
100            let arb_token = state.current().unwrap();
101            let arb_range = arb_token.span;
102            let arb_text = state.source.get_text_in(arb_range).to_string();
103
104            let arb_cp = state.checkpoint();
105            state.bump();
106            state.finish_at(arb_cp, TailwindElementType::ArbitraryValue);
107
108            TailwindClassKind::ArbitraryValue(TailwindArbitraryValue { span: arb_range, value: arb_text })
109        }
110        else {
111            // Fallback for incomplete class
112            TailwindClassKind::Utility(TailwindUtility { span: Range { start: state.current_offset(), end: state.current_offset() }, name: String::new() })
113        };
114
115        // Optional important flag at end
116        if state.eat(TailwindTokenType::Important) {
117            is_important = true;
118        }
119
120        state.finish_at(checkpoint, TailwindElementType::Class);
121        let end_pos = state.current_offset();
122
123        Ok(TailwindClass { span: Range { start: start_pos, end: end_pos }, is_important, modifiers, kind })
124    }
125
126    /// Parses a directive (e.g., @tailwind base).
127    fn parse_directive<'a, S: Source + ?Sized>(&self, state: &mut State<'a, S>) -> Result<TailwindDirective, OakError> {
128        let checkpoint = state.checkpoint();
129        let start_pos = state.current_offset();
130
131        let dir_token = state.current().unwrap();
132        let name = state.source.get_text_in(dir_token.span).to_string();
133        state.expect(TailwindTokenType::Directive)?;
134
135        let mut body_parts = Vec::new();
136        // Consume anything until semicolon or end of line/file
137        while state.not_at_end() && !state.at(TailwindTokenType::Semicolon) {
138            if let Some(token) = state.current() {
139                body_parts.push(state.source.get_text_in(token.span).to_string());
140            }
141            state.bump();
142        }
143
144        let body = if body_parts.is_empty() { None } else { Some(body_parts.join("")) };
145
146        state.eat(TailwindTokenType::Semicolon);
147        state.finish_at(checkpoint, TailwindElementType::Directive);
148        let end_pos = state.current_offset();
149
150        Ok(TailwindDirective { span: Range { start: start_pos, end: end_pos }, name, body })
151    }
152}
153
154impl Parser<TailwindLanguage> for TailwindParser {
155    /// Parses the source text into a Tailwind syntax tree.
156    fn parse<'a, S: Source + ?Sized>(&self, text: &'a S, edits: &[TextEdit], cache: &'a mut impl ParseCache<TailwindLanguage>) -> ParseOutput<'a, TailwindLanguage> {
157        let lexer = TailwindLexer::new(self.config);
158        let mut ast_nodes = Vec::new();
159
160        let output = parse_with_lexer(&lexer, text, edits, cache, |state| {
161            let checkpoint = state.checkpoint();
162            let start_pos = state.current_offset();
163
164            while state.not_at_end() {
165                if let Ok(Some(node)) = self.parse_node(state) {
166                    ast_nodes.push(node);
167                }
168            }
169
170            let root_range = Range { start: start_pos, end: state.current_offset() };
171            let _root_ast = TailwindRoot::new(root_range, ast_nodes);
172            // Note: In a real implementation, we might want to store the AST somewhere or return it.
173            // For now, we follow the Oak pattern of focusing on the GreenNode.
174
175            Ok(state.finish_at(checkpoint, TailwindElementType::Root))
176        });
177
178        output
179    }
180}