hypen_parser/
parser.rs

1use chumsky::prelude::*;
2
3use crate::ast::*;
4
5/// Parse a line comment: // ... until end of line
6fn line_comment<'a>() -> impl Parser<'a, &'a str, (), extra::Err<Rich<'a, char>>> + Clone {
7    just("//")
8        .then(none_of('\n').repeated())
9        .ignored()
10}
11
12/// Parse a block comment: /* ... */
13fn block_comment<'a>() -> impl Parser<'a, &'a str, (), extra::Err<Rich<'a, char>>> + Clone {
14    just("/*")
15        .then(any().and_is(just("*/").not()).repeated())
16        .then(just("*/"))
17        .ignored()
18}
19
20/// Parse any comment (line or block)
21fn comment<'a>() -> impl Parser<'a, &'a str, (), extra::Err<Rich<'a, char>>> + Clone {
22    line_comment().or(block_comment())
23}
24
25/// Parse whitespace and comments (replaces .padded() for comment support)
26fn ws<'a>() -> impl Parser<'a, &'a str, (), extra::Err<Rich<'a, char>>> + Clone {
27    text::whitespace()
28        .then(comment().then(text::whitespace()).repeated())
29        .ignored()
30}
31
32/// Extension trait to add comment-aware padding to parsers
33trait PaddedWithComments<'a, O>: Parser<'a, &'a str, O, extra::Err<Rich<'a, char>>> + Sized {
34    fn padded_with_comments(self) -> impl Parser<'a, &'a str, O, extra::Err<Rich<'a, char>>> + Clone
35    where
36        Self: Clone,
37    {
38        ws().ignore_then(self).then_ignore(ws())
39    }
40}
41
42impl<'a, O, P> PaddedWithComments<'a, O> for P where P: Parser<'a, &'a str, O, extra::Err<Rich<'a, char>>> {}
43
44/// Parse a Hypen value (string, number, boolean, reference, list, map)
45fn value_parser<'a>() -> impl Parser<'a, &'a str, Value, extra::Err<Rich<'a, char>>> + Clone {
46    recursive(|value| {
47        // String literal: "hello world"
48        let string = just('"')
49            .ignore_then(none_of('"').repeated())
50            .then_ignore(just('"'))
51            .to_slice()
52            .map(|s: &str| Value::String(s.to_string()));
53
54        // Boolean: true or false
55        let boolean = text::keyword("true")
56            .to(Value::Boolean(true))
57            .or(text::keyword("false").to(Value::Boolean(false)));
58
59        // Number: 123, 123.45
60        let number = text::int(10)
61            .then(just('.').then(text::digits(10)).or_not())
62            .to_slice()
63            .map(|s: &str| Value::Number(s.parse().unwrap()));
64
65        // Reference: @state.user, @actions.login
66        let reference = just('@')
67            .ignore_then(
68                text::ascii::ident()
69                    .then(just('.').ignore_then(text::ascii::ident()).repeated())
70                    .to_slice(),
71            )
72            .map(|s: &str| Value::Reference(s.to_string()));
73
74        // List: [item1, item2, item3]
75        let list = value
76            .clone()
77            .padded_with_comments()
78            .separated_by(just(','))
79            .allow_trailing()
80            .collect()
81            .delimited_by(just('['), just(']'))
82            .map(Value::List);
83
84        // Map: {key1: value1, key2: value2}
85        let map_entry = text::ascii::ident()
86            .padded_with_comments()
87            .then_ignore(just(':'))
88            .then(value.clone().padded_with_comments());
89
90        let map = map_entry
91            .separated_by(just(','))
92            .allow_trailing()
93            .collect::<Vec<_>>()
94            .delimited_by(just('{'), just('}'))
95            .map(|entries: Vec<(&str, Value)>| {
96                Value::Map(entries.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
97            });
98
99        // Bare identifier (unquoted string for simple cases)
100        let identifier = text::ascii::ident()
101            .map(|s: &str| Value::String(s.to_string()));
102
103        choice((string, reference, boolean, number, list, map, identifier))
104    })
105}
106
107/// Parse a complete component with optional children
108pub fn component_parser<'a>(
109) -> impl Parser<'a, &'a str, ComponentSpecification, extra::Err<Rich<'a, char>>> + Clone {
110    recursive(|component| {
111        // Optional declaration keyword: "module" or "component"
112        let declaration_keyword = text::keyword("module")
113            .to(DeclarationType::Module)
114            .or(text::keyword("component").to(DeclarationType::ComponentKeyword))
115            .padded_with_comments()
116            .or_not();
117
118        // Component name (supports dot notation for applicators)
119        let name = just('.')
120            .or_not()
121            .then(text::ascii::ident())
122            .to_slice()
123            .map(|s: &str| s.to_string())
124            .padded_with_comments();
125
126        // Parse single argument (named or positional)
127        let value = value_parser();
128
129        let arg = text::ascii::ident()
130            .then_ignore(just(':').padded_with_comments())
131            .then(value.clone())
132            .map(|(key, value)| (Some(key.to_string()), value))
133            .or(value.map(|value| (None, value)));
134
135        // Arguments parser
136        let arg_parser = arg
137            .clone()
138            .padded_with_comments()
139            .separated_by(just(','))
140            .allow_trailing()
141            .collect::<Vec<_>>()
142            .delimited_by(just('('), just(')'))
143            .map(|args| {
144                let arguments = args
145                    .into_iter()
146                    .enumerate()
147                    .map(|(i, (key, value))| match key {
148                        Some(k) => Argument::Named { key: k, value },
149                        None => Argument::Positioned { position: i, value },
150                    })
151                    .collect();
152                ArgumentList::new(arguments)
153            })
154            .or(empty().to(ArgumentList::empty()));
155
156        let args = arg_parser.clone().padded_with_comments().or_not();
157
158        // Children block: { child1 child2 child3 }
159        let children_block = component
160            .clone()
161            .padded_with_comments()
162            .repeated()
163            .collect::<Vec<_>>()
164            .delimited_by(just('{'), just('}'))
165            .padded_with_comments()
166            .or_not();
167
168        // Applicators: .applicator1() .applicator2(args)
169        let applicators = just('.')
170            .ignore_then(text::ascii::ident())
171            .then(arg_parser.clone())
172            .map(|(name, args)| ApplicatorSpecification {
173                name: name.to_string(),
174                arguments: args,
175                children: vec![],
176                internal_id: String::new(),
177            })
178            .padded_with_comments()
179            .repeated()
180            .collect::<Vec<_>>();
181
182        declaration_keyword
183            .then(name)
184            .then(args)
185            .then(children_block)
186            .then(applicators)
187            .map(|((((decl_type, name), args), children), applicators)| {
188                // Fold applicators into the component hierarchy
189                let base_component = ComponentSpecification::new(
190                    uuid::Uuid::new_v4().to_string(),
191                    name.clone(),
192                    args.unwrap_or_else(ArgumentList::empty),
193                    vec![],
194                    fold_applicators(children.unwrap_or_default()),
195                    MetaData {
196                        internal_id: String::new(),
197                        name_range: 0..0,
198                        block_range: None,
199                    },
200                )
201                .with_declaration_type(decl_type.unwrap_or(DeclarationType::Component));
202
203                // If there are applicators, add them to the component
204                if applicators.is_empty() {
205                    base_component
206                } else {
207                    ComponentSpecification {
208                        applicators,
209                        ..base_component
210                    }
211                }
212            })
213    })
214}
215
216/// Fold applicators into component hierarchy
217/// Components starting with '.' are treated as applicators of the previous component
218fn fold_applicators(components: Vec<ComponentSpecification>) -> Vec<ComponentSpecification> {
219    let mut result: Vec<ComponentSpecification> = Vec::new();
220
221    for component in components {
222        if component.name.starts_with('.') && !result.is_empty() {
223            // This is an applicator - attach it to the previous component
224            let mut owner: ComponentSpecification = result.pop().unwrap();
225            owner.applicators.push(component.to_applicator());
226            result.push(owner);
227        } else {
228            result.push(component);
229        }
230    }
231
232    result
233}
234
235/// Parse an import statement
236/// Syntax: import { Component1, Component2 } from "path"
237///     or: import Component from "path"
238pub fn import_parser<'a>() -> impl Parser<'a, &'a str, ImportStatement, extra::Err<Rich<'a, char>>> + Clone {
239    // Parse import keyword
240    let import_keyword = text::keyword("import").padded_with_comments();
241
242    // Parse a string literal for the source path (properly removing quotes)
243    let string_literal = just('"')
244        .ignore_then(none_of('"').repeated().to_slice())
245        .then_ignore(just('"'))
246        .map(|s: &str| s.to_string());
247
248    // Parse named imports: { Component1, Component2, ... }
249    let named_imports = text::ascii::ident()
250        .map(|s: &str| s.to_string())
251        .padded_with_comments()
252        .separated_by(just(','))
253        .allow_trailing()
254        .collect::<Vec<String>>()
255        .delimited_by(just('{').padded_with_comments(), just('}').padded_with_comments())
256        .map(ImportClause::Named);
257
258    // Parse default import: ComponentName
259    let default_import = text::ascii::ident()
260        .map(|s: &str| s.to_string())
261        .map(ImportClause::Default);
262
263    // Import clause can be either named or default
264    let import_clause = named_imports.or(default_import).padded_with_comments();
265
266    // Parse "from" keyword
267    let from_keyword = text::keyword("from").padded_with_comments();
268
269    // Parse the full import statement
270    import_keyword
271        .ignore_then(import_clause)
272        .then_ignore(from_keyword)
273        .then(string_literal.padded_with_comments())
274        .map(|(clause, source_str)| {
275            // Determine if source is a URL or local path
276            let source = if source_str.starts_with("http://") || source_str.starts_with("https://") {
277                ImportSource::Url(source_str)
278            } else {
279                ImportSource::Local(source_str)
280            };
281            ImportStatement::new(clause, source)
282        })
283}
284
285/// Parse a complete Hypen document with imports and components
286pub fn document_parser<'a>() -> impl Parser<'a, &'a str, Document, extra::Err<Rich<'a, char>>> + Clone {
287    // Parse imports (zero or more)
288    let imports = import_parser()
289        .padded_with_comments()
290        .repeated()
291        .collect::<Vec<ImportStatement>>();
292
293    // Parse components (zero or more)
294    let components = component_parser()
295        .padded_with_comments()
296        .repeated()
297        .collect::<Vec<ComponentSpecification>>();
298
299    // Combine imports and components into a document
300    imports
301        .then(components)
302        .map(|(imports, components)| Document::new(imports, components))
303}
304
305/// Parse a list of components from text
306pub fn parse_components(
307    input: &str,
308) -> Result<Vec<ComponentSpecification>, Vec<Rich<char>>> {
309    component_parser()
310        .padded_with_comments()
311        .repeated()
312        .collect()
313        .then_ignore(end())
314        .parse(input)
315        .into_result()
316}
317
318/// Parse a single component from text
319pub fn parse_component(
320    input: &str,
321) -> Result<ComponentSpecification, Vec<Rich<char>>> {
322    component_parser()
323        .padded_with_comments()
324        .then_ignore(end())
325        .parse(input)
326        .into_result()
327}
328
329/// Parse a complete Hypen document (imports + components)
330pub fn parse_document(
331    input: &str,
332) -> Result<Document, Vec<Rich<char>>> {
333    document_parser()
334        .padded_with_comments()
335        .then_ignore(end())
336        .parse(input)
337        .into_result()
338}
339
340/// Parse a single import statement
341pub fn parse_import(
342    input: &str,
343) -> Result<ImportStatement, Vec<Rich<char>>> {
344    import_parser()
345        .padded_with_comments()
346        .then_ignore(end())
347        .parse(input)
348        .into_result()
349}
350
351// Simple UUID generation
352mod uuid {
353    use std::sync::atomic::{AtomicUsize, Ordering};
354
355    static COUNTER: AtomicUsize = AtomicUsize::new(0);
356
357    pub struct Uuid(usize);
358
359    impl Uuid {
360        pub fn new_v4() -> Self {
361            Uuid(COUNTER.fetch_add(1, Ordering::SeqCst))
362        }
363
364        pub fn to_string(&self) -> String {
365            format!("id-{}", self.0)
366        }
367    }
368}