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        // Parse explicitly to handle empty braces { }
160        let children_block = just('{')
161            .padded_with_comments()
162            .ignore_then(
163                component
164                    .clone()
165                    .padded_with_comments()
166                    .repeated()
167                    .collect::<Vec<_>>()
168            )
169            .then_ignore(just('}').padded_with_comments())
170            .or_not();
171
172        // Applicators: .applicator1() .applicator2(args)
173        let applicators = just('.')
174            .ignore_then(text::ascii::ident())
175            .then(arg_parser.clone())
176            .map(|(name, args)| ApplicatorSpecification {
177                name: name.to_string(),
178                arguments: args,
179                children: vec![],
180                internal_id: String::new(),
181            })
182            .padded_with_comments()
183            .repeated()
184            .collect::<Vec<_>>();
185
186        declaration_keyword
187            .then(name)
188            .then(args)
189            .then(children_block)
190            .then(applicators)
191            .map(|((((decl_type, name), args), children), applicators)| {
192                // Fold applicators into the component hierarchy
193                let base_component = ComponentSpecification::new(
194                    uuid::Uuid::new_v4().to_string(),
195                    name.clone(),
196                    args.unwrap_or_else(ArgumentList::empty),
197                    vec![],
198                    fold_applicators(children.unwrap_or_default()),
199                    MetaData {
200                        internal_id: String::new(),
201                        name_range: 0..0,
202                        block_range: None,
203                    },
204                )
205                .with_declaration_type(decl_type.unwrap_or(DeclarationType::Component));
206
207                // If there are applicators, add them to the component
208                if applicators.is_empty() {
209                    base_component
210                } else {
211                    ComponentSpecification {
212                        applicators,
213                        ..base_component
214                    }
215                }
216            })
217    })
218}
219
220/// Fold applicators into component hierarchy
221/// Components starting with '.' are treated as applicators of the previous component
222fn fold_applicators(components: Vec<ComponentSpecification>) -> Vec<ComponentSpecification> {
223    let mut result: Vec<ComponentSpecification> = Vec::new();
224
225    for component in components {
226        if component.name.starts_with('.') && !result.is_empty() {
227            // This is an applicator - attach it to the previous component
228            let mut owner: ComponentSpecification = result.pop().unwrap();
229            owner.applicators.push(component.to_applicator());
230            result.push(owner);
231        } else {
232            result.push(component);
233        }
234    }
235
236    result
237}
238
239/// Parse an import statement
240/// Syntax: import { Component1, Component2 } from "path"
241///     or: import Component from "path"
242pub fn import_parser<'a>() -> impl Parser<'a, &'a str, ImportStatement, extra::Err<Rich<'a, char>>> + Clone {
243    // Parse import keyword
244    let import_keyword = text::keyword("import").padded_with_comments();
245
246    // Parse a string literal for the source path (properly removing quotes)
247    let string_literal = just('"')
248        .ignore_then(none_of('"').repeated().to_slice())
249        .then_ignore(just('"'))
250        .map(|s: &str| s.to_string());
251
252    // Parse named imports: { Component1, Component2, ... }
253    let named_imports = text::ascii::ident()
254        .map(|s: &str| s.to_string())
255        .padded_with_comments()
256        .separated_by(just(','))
257        .allow_trailing()
258        .collect::<Vec<String>>()
259        .delimited_by(just('{').padded_with_comments(), just('}').padded_with_comments())
260        .map(ImportClause::Named);
261
262    // Parse default import: ComponentName
263    let default_import = text::ascii::ident()
264        .map(|s: &str| s.to_string())
265        .map(ImportClause::Default);
266
267    // Import clause can be either named or default
268    let import_clause = named_imports.or(default_import).padded_with_comments();
269
270    // Parse "from" keyword
271    let from_keyword = text::keyword("from").padded_with_comments();
272
273    // Parse the full import statement
274    import_keyword
275        .ignore_then(import_clause)
276        .then_ignore(from_keyword)
277        .then(string_literal.padded_with_comments())
278        .map(|(clause, source_str)| {
279            // Determine if source is a URL or local path
280            let source = if source_str.starts_with("http://") || source_str.starts_with("https://") {
281                ImportSource::Url(source_str)
282            } else {
283                ImportSource::Local(source_str)
284            };
285            ImportStatement::new(clause, source)
286        })
287}
288
289/// Parse a complete Hypen document with imports and components
290pub fn document_parser<'a>() -> impl Parser<'a, &'a str, Document, extra::Err<Rich<'a, char>>> + Clone {
291    // Parse imports (zero or more)
292    let imports = import_parser()
293        .padded_with_comments()
294        .repeated()
295        .collect::<Vec<ImportStatement>>();
296
297    // Parse components (zero or more)
298    let components = component_parser()
299        .padded_with_comments()
300        .repeated()
301        .collect::<Vec<ComponentSpecification>>();
302
303    // Combine imports and components into a document
304    imports
305        .then(components)
306        .map(|(imports, components)| Document::new(imports, components))
307}
308
309/// Parse a list of components from text
310pub fn parse_components(
311    input: &str,
312) -> Result<Vec<ComponentSpecification>, Vec<Rich<char>>> {
313    component_parser()
314        .padded_with_comments()
315        .repeated()
316        .collect()
317        .then_ignore(end())
318        .parse(input)
319        .into_result()
320}
321
322/// Parse a single component from text
323pub fn parse_component(
324    input: &str,
325) -> Result<ComponentSpecification, Vec<Rich<char>>> {
326    component_parser()
327        .padded_with_comments()
328        .then_ignore(end())
329        .parse(input)
330        .into_result()
331}
332
333/// Parse a complete Hypen document (imports + components)
334pub fn parse_document(
335    input: &str,
336) -> Result<Document, Vec<Rich<char>>> {
337    document_parser()
338        .padded_with_comments()
339        .then_ignore(end())
340        .parse(input)
341        .into_result()
342}
343
344/// Parse a single import statement
345pub fn parse_import(
346    input: &str,
347) -> Result<ImportStatement, Vec<Rich<char>>> {
348    import_parser()
349        .padded_with_comments()
350        .then_ignore(end())
351        .parse(input)
352        .into_result()
353}
354
355// Simple UUID generation
356mod uuid {
357    use std::sync::atomic::{AtomicUsize, Ordering};
358
359    static COUNTER: AtomicUsize = AtomicUsize::new(0);
360
361    pub struct Uuid(usize);
362
363    impl Uuid {
364        pub fn new_v4() -> Self {
365            Uuid(COUNTER.fetch_add(1, Ordering::SeqCst))
366        }
367
368        pub fn to_string(&self) -> String {
369            format!("id-{}", self.0)
370        }
371    }
372}