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