Skip to main content

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    // Match zero or more of: (whitespace | comment)
28    // This avoids generating confusing "expected '/'" errors
29    choice((
30        text::whitespace().at_least(1).ignored(),
31        comment(),
32    ))
33    .repeated()
34    .ignored()
35}
36
37/// Extension trait to add comment-aware padding to parsers
38trait PaddedWithComments<'a, O>: Parser<'a, &'a str, O, extra::Err<Rich<'a, char>>> + Sized {
39    fn padded_with_comments(self) -> impl Parser<'a, &'a str, O, extra::Err<Rich<'a, char>>> + Clone
40    where
41        Self: Clone,
42    {
43        ws().ignore_then(self).then_ignore(ws())
44    }
45}
46
47impl<'a, O, P> PaddedWithComments<'a, O> for P where P: Parser<'a, &'a str, O, extra::Err<Rich<'a, char>>> {}
48
49/// Parse a Hypen value (string, number, boolean, reference, list, map)
50fn value_parser<'a>() -> impl Parser<'a, &'a str, Value, extra::Err<Rich<'a, char>>> + Clone {
51    recursive(|value| {
52        // String literal: "hello world"
53        let string = just('"')
54            .ignore_then(none_of('"').repeated())
55            .then_ignore(just('"').labelled("closing quote '\"'"))
56            .to_slice()
57            .map(|s: &str| Value::String(s.to_string()))
58            .labelled("string literal");
59
60        // Boolean: true or false
61        let boolean = text::keyword("true")
62            .to(Value::Boolean(true))
63            .or(text::keyword("false").to(Value::Boolean(false)))
64            .labelled("boolean (true or false)");
65
66        // Number: 123, 123.45, -123, -123.45
67        let number = just('-')
68            .or_not()
69            .then(text::int(10))
70            .then(just('.').then(text::digits(10)).or_not())
71            .to_slice()
72            .map(|s: &str| Value::Number(s.parse().unwrap()))
73            .labelled("number");
74
75        // Reference: @state.user, @actions.login
76        let reference = just('@')
77            .ignore_then(
78                text::ascii::ident()
79                    .labelled("reference path (e.g., state.user)")
80                    .then(just('.').ignore_then(text::ascii::ident()).repeated())
81                    .to_slice(),
82            )
83            .map(|s: &str| Value::Reference(s.to_string()))
84            .labelled("reference (@state.* or @actions.*)");
85
86        // List: [item1, item2, item3]
87        let list = value
88            .clone()
89            .padded_with_comments()
90            .separated_by(just(','))
91            .allow_trailing()
92            .collect()
93            .delimited_by(just('['), just(']').labelled("closing bracket ']'"))
94            .map(Value::List)
95            .labelled("list [...]");
96
97        // Map: {key1: value1, key2: value2}
98        let map_entry = text::ascii::ident()
99            .labelled("map key")
100            .padded_with_comments()
101            .then_ignore(just(':').labelled("':' after map key"))
102            .then(value.clone().padded_with_comments().labelled("map value"));
103
104        let map = map_entry
105            .separated_by(just(','))
106            .allow_trailing()
107            .collect::<Vec<_>>()
108            .delimited_by(just('{'), just('}').labelled("closing brace '}'"))
109            .map(|entries: Vec<(&str, Value)>| {
110                Value::Map(entries.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
111            })
112            .labelled("map {...}");
113
114        // Bare identifier (unquoted string for simple cases)
115        let identifier = text::ascii::ident()
116            .map(|s: &str| Value::String(s.to_string()))
117            .labelled("identifier");
118
119        choice((string, reference, boolean, number, list, map, identifier))
120            .labelled("value")
121    })
122}
123
124/// Parse a complete component with optional children
125pub fn component_parser<'a>(
126) -> impl Parser<'a, &'a str, ComponentSpecification, extra::Err<Rich<'a, char>>> + Clone {
127    recursive(|component| {
128        // Optional declaration keyword: "module" or "component"
129        let declaration_keyword = text::keyword("module")
130            .to(DeclarationType::Module)
131            .or(text::keyword("component").to(DeclarationType::ComponentKeyword))
132            .labelled("declaration keyword (module or component)")
133            .padded_with_comments()
134            .or_not();
135
136        // Component name (supports dot notation for applicators)
137        let name = just('.')
138            .or_not()
139            .then(text::ascii::ident())
140            .to_slice()
141            .map(|s: &str| s.to_string())
142            .labelled("component name")
143            .padded_with_comments();
144
145        // Parse single argument (named or positional)
146        let value = value_parser();
147
148        let arg = text::ascii::ident()
149            .then_ignore(just(':').padded_with_comments())
150            .then(value.clone())
151            .map(|(key, value)| (Some(key.to_string()), value))
152            .labelled("named argument (key: value)")
153            .or(value.map(|value| (None, value)).labelled("positional argument"));
154
155        // Arguments parser - only parses (arg1, arg2, ...) when present
156        let args_with_parens = arg
157            .clone()
158            .padded_with_comments()
159            .separated_by(just(',').labelled("',' between arguments"))
160            .allow_trailing()
161            .collect::<Vec<_>>()
162            .delimited_by(
163                just('(').labelled("'(' to start arguments"),
164                just(')').labelled("closing parenthesis ')'")
165            )
166            .map(|args| {
167                let arguments = args
168                    .into_iter()
169                    .enumerate()
170                    .map(|(i, (key, value))| match key {
171                        Some(k) => Argument::Named { key: k, value },
172                        None => Argument::Positioned { position: i, value },
173                    })
174                    .collect();
175                ArgumentList::new(arguments)
176            })
177            .labelled("argument list (...)");
178
179        // Arguments are optional - either we have (...) or nothing
180        let args = args_with_parens
181            .or_not()
182            .map(|opt| opt.unwrap_or_else(ArgumentList::empty));
183
184        // Also keep arg_parser for applicators
185        let arg_parser = arg
186            .clone()
187            .padded_with_comments()
188            .separated_by(just(',').labelled("',' between arguments"))
189            .allow_trailing()
190            .collect::<Vec<_>>()
191            .delimited_by(
192                just('('),
193                just(')').labelled("closing parenthesis ')'")
194            )
195            .map(|args| {
196                let arguments = args
197                    .into_iter()
198                    .enumerate()
199                    .map(|(i, (key, value))| match key {
200                        Some(k) => Argument::Named { key: k, value },
201                        None => Argument::Positioned { position: i, value },
202                    })
203                    .collect();
204                ArgumentList::new(arguments)
205            })
206            .or(empty().to(ArgumentList::empty()));
207
208        // Children block: { child1 child2 child3 }
209        // Parse explicitly to handle empty braces { }
210        let children_block = just('{')
211            .padded_with_comments()
212            .ignore_then(
213                component
214                    .clone()
215                    .padded_with_comments()
216                    .repeated()
217                    .collect::<Vec<_>>()
218            )
219            .then_ignore(just('}').labelled("closing brace '}' for children block").padded_with_comments())
220            .labelled("children block {...}")
221            .or_not();
222
223        // Applicators: .applicator1() .applicator2(args)
224        let applicators = just('.')
225            .ignore_then(text::ascii::ident().labelled("applicator name"))
226            .then(arg_parser.clone())
227            .map(|(name, args)| ApplicatorSpecification {
228                name: name.to_string(),
229                arguments: args,
230                children: vec![],
231                internal_id: String::new(),
232            })
233            .labelled("applicator (.name(...))")
234            .padded_with_comments()
235            .repeated()
236            .collect::<Vec<_>>();
237
238        declaration_keyword
239            .then(name)
240            .then(args)
241            .then(children_block)
242            .then(applicators)
243            .map(|((((decl_type, name), args), children), applicators)| {
244                // Fold applicators into the component hierarchy
245                let base_component = ComponentSpecification::new(
246                    id_gen::NodeId::next().to_string(),
247                    name.clone(),
248                    args,  // args is already an ArgumentList, not Option
249                    vec![],
250                    fold_applicators(children.unwrap_or_default()),
251                    MetaData {
252                        internal_id: String::new(),
253                        name_range: 0..0,
254                        block_range: None,
255                    },
256                )
257                .with_declaration_type(decl_type.unwrap_or(DeclarationType::Component));
258
259                // If there are applicators, add them to the component
260                if applicators.is_empty() {
261                    base_component
262                } else {
263                    ComponentSpecification {
264                        applicators,
265                        ..base_component
266                    }
267                }
268            })
269            .labelled("component")
270    })
271}
272
273/// Fold applicators into component hierarchy
274/// Components starting with '.' are treated as applicators of the previous component
275fn fold_applicators(components: Vec<ComponentSpecification>) -> Vec<ComponentSpecification> {
276    let mut result: Vec<ComponentSpecification> = Vec::new();
277
278    for component in components {
279        if component.name.starts_with('.') && !result.is_empty() {
280            // This is an applicator - attach it to the previous component
281            let mut owner: ComponentSpecification = result.pop().unwrap();
282            owner.applicators.push(component.to_applicator());
283            result.push(owner);
284        } else {
285            result.push(component);
286        }
287    }
288
289    result
290}
291
292/// Parse an import statement
293/// Syntax: import { Component1, Component2 } from "path"
294///     or: import Component from "path"
295pub fn import_parser<'a>() -> impl Parser<'a, &'a str, ImportStatement, extra::Err<Rich<'a, char>>> + Clone {
296    // Parse import keyword
297    let import_keyword = text::keyword("import")
298        .labelled("'import' keyword")
299        .padded_with_comments();
300
301    // Parse a string literal for the source path (properly removing quotes)
302    let string_literal = just('"')
303        .ignore_then(none_of('"').repeated().to_slice())
304        .then_ignore(just('"').labelled("closing quote '\"'"))
305        .map(|s: &str| s.to_string())
306        .labelled("import path string");
307
308    // Parse named imports: { Component1, Component2, ... }
309    let named_imports = text::ascii::ident()
310        .map(|s: &str| s.to_string())
311        .labelled("component name")
312        .padded_with_comments()
313        .separated_by(just(','))
314        .allow_trailing()
315        .collect::<Vec<String>>()
316        .delimited_by(
317            just('{').padded_with_comments(),
318            just('}').labelled("closing brace '}' for named imports").padded_with_comments()
319        )
320        .map(ImportClause::Named)
321        .labelled("named imports { ... }");
322
323    // Parse default import: ComponentName
324    let default_import = text::ascii::ident()
325        .map(|s: &str| s.to_string())
326        .map(ImportClause::Default)
327        .labelled("default import name");
328
329    // Import clause can be either named or default
330    let import_clause = named_imports.or(default_import).padded_with_comments();
331
332    // Parse "from" keyword
333    let from_keyword = text::keyword("from")
334        .labelled("'from' keyword")
335        .padded_with_comments();
336
337    // Parse the full import statement
338    import_keyword
339        .ignore_then(import_clause)
340        .then_ignore(from_keyword)
341        .then(string_literal.padded_with_comments())
342        .map(|(clause, source_str)| {
343            // Determine if source is a URL or local path
344            let source = if source_str.starts_with("http://") || source_str.starts_with("https://") {
345                ImportSource::Url(source_str)
346            } else {
347                ImportSource::Local(source_str)
348            };
349            ImportStatement::new(clause, source)
350        })
351        .labelled("import statement")
352}
353
354/// Parse a complete Hypen document with imports and components
355pub fn document_parser<'a>() -> impl Parser<'a, &'a str, Document, extra::Err<Rich<'a, char>>> + Clone {
356    // Parse imports (zero or more)
357    let imports = import_parser()
358        .padded_with_comments()
359        .repeated()
360        .collect::<Vec<ImportStatement>>();
361
362    // Parse components (zero or more)
363    let components = component_parser()
364        .padded_with_comments()
365        .repeated()
366        .collect::<Vec<ComponentSpecification>>();
367
368    // Combine imports and components into a document
369    imports
370        .then(components)
371        .map(|(imports, components)| Document::new(imports, components))
372}
373
374/// Parse a list of components from text
375pub fn parse_components(
376    input: &str,
377) -> Result<Vec<ComponentSpecification>, Vec<Rich<char>>> {
378    component_parser()
379        .padded_with_comments()
380        .repeated()
381        .collect()
382        .then_ignore(end())
383        .parse(input)
384        .into_result()
385}
386
387/// Parse a single component from text
388pub fn parse_component(
389    input: &str,
390) -> Result<ComponentSpecification, Vec<Rich<char>>> {
391    component_parser()
392        .padded_with_comments()
393        .then_ignore(end())
394        .parse(input)
395        .into_result()
396}
397
398/// Parse a complete Hypen document (imports + components)
399pub fn parse_document(
400    input: &str,
401) -> Result<Document, Vec<Rich<char>>> {
402    document_parser()
403        .padded_with_comments()
404        .then_ignore(end())
405        .parse(input)
406        .into_result()
407}
408
409/// Parse a single import statement
410pub fn parse_import(
411    input: &str,
412) -> Result<ImportStatement, Vec<Rich<char>>> {
413    import_parser()
414        .padded_with_comments()
415        .then_ignore(end())
416        .parse(input)
417        .into_result()
418}
419
420// Simple sequential ID generator for AST nodes
421mod id_gen {
422    use std::sync::atomic::{AtomicUsize, Ordering};
423
424    static COUNTER: AtomicUsize = AtomicUsize::new(0);
425
426    pub struct NodeId(usize);
427
428    impl NodeId {
429        /// Generate the next sequential ID
430        pub fn next() -> Self {
431            NodeId(COUNTER.fetch_add(1, Ordering::SeqCst))
432        }
433
434        pub fn to_string(&self) -> String {
435            format!("id-{}", self.0)
436        }
437    }
438}