1use crate::id::NodeId;
9use crate::model::*;
10use winnow::ascii::space1;
11use winnow::combinator::{alt, delimited, opt, preceded};
12use winnow::error::ContextError;
13use winnow::prelude::*;
14use winnow::token::{take_till, take_while};
15
16#[must_use = "parsing result should be used"]
18pub fn parse_document(input: &str) -> Result<SceneGraph, String> {
19 let mut graph = SceneGraph::new();
20 let mut rest = input;
21
22 let mut pending_comments = collect_leading_comments(&mut rest);
24
25 while !rest.is_empty() {
26 let line = line_number(input, rest);
27 let end = {
28 let max = rest.len().min(40);
29 let mut e = max;
31 while e > 0 && !rest.is_char_boundary(e) {
32 e -= 1;
33 }
34 e
35 };
36 let ctx = &rest[..end];
37
38 if rest.starts_with("import ") {
39 let import = parse_import_line
40 .parse_next(&mut rest)
41 .map_err(|e| format!("line {line}: import error — expected `import \"path\" as name`, got `{ctx}…`: {e}"))?;
42 graph.imports.push(import);
43 pending_comments.clear();
44 } else if rest.starts_with("style ") || rest.starts_with("theme ") {
45 let (name, style) = parse_style_block
46 .parse_next(&mut rest)
47 .map_err(|e| format!("line {line}: theme/style error — expected `theme name {{ props }}`, got `{ctx}…`: {e}"))?;
48 graph.define_style(name, style);
49 pending_comments.clear();
50 } else if rest.starts_with("spec ") || rest.starts_with("spec{") {
51 let _ = parse_spec_block.parse_next(&mut rest);
53 pending_comments.clear();
54 } else if rest.starts_with('@') {
55 if is_generic_node_start(rest) {
56 let mut node_data = parse_node.parse_next(&mut rest).map_err(|e| {
57 format!("line {line}: node error — expected `@id {{ ... }}`, got `{ctx}…`: {e}")
58 })?;
59 node_data.comments = std::mem::take(&mut pending_comments);
60 let root = graph.root;
61 insert_node_recursive(&mut graph, root, node_data);
62 } else {
63 let (node_id, constraint) = parse_constraint_line
64 .parse_next(&mut rest)
65 .map_err(|e| format!("line {line}: constraint error — expected `@id -> type: value`, got `{ctx}…`: {e}"))?;
66 if let Some(node) = graph.get_by_id_mut(node_id) {
67 node.constraints.push(constraint);
68 }
69 pending_comments.clear();
70 }
71 } else if rest.starts_with("edge ") {
72 let (edge, text_child_data) = parse_edge_block
73 .parse_next(&mut rest)
74 .map_err(|e| format!("line {line}: edge error — expected `edge @id {{ from: @a to: @b }}`, got `{ctx}…`: {e}"))?;
75 if let Some((text_id, content)) = text_child_data {
77 let text_node = crate::model::SceneNode {
78 id: text_id,
79 kind: crate::model::NodeKind::Text {
80 content,
81 max_width: None,
82 },
83 style: crate::model::Style::default(),
84 use_styles: Default::default(),
85 constraints: Default::default(),
86 annotations: Vec::new(),
87 animations: Default::default(),
88 comments: Vec::new(),
89 place: None,
90 };
91 let idx = graph.graph.add_node(text_node);
92 graph.graph.add_edge(graph.root, idx, ());
93 graph.id_index.insert(text_id, idx);
94 }
95 graph.edges.push(edge);
96 pending_comments.clear();
97 } else if starts_with_node_keyword(rest) {
98 let mut node_data = parse_node.parse_next(&mut rest).map_err(|e| {
99 format!(
100 "line {line}: node error — expected `kind @id {{ ... }}`, got `{ctx}…`: {e}"
101 )
102 })?;
103 node_data.comments = std::mem::take(&mut pending_comments);
104 let root = graph.root;
105 insert_node_recursive(&mut graph, root, node_data);
106 } else {
107 let _ = take_till::<_, _, ContextError>(0.., '\n').parse_next(&mut rest);
109 if rest.starts_with('\n') {
110 rest = &rest[1..];
111 }
112 pending_comments.clear();
113 }
114
115 let more = collect_leading_comments(&mut rest);
118 pending_comments.extend(more);
119 }
120
121 Ok(graph)
122}
123
124fn line_number(full_input: &str, remaining: &str) -> usize {
126 let consumed = full_input.len() - remaining.len();
127 full_input[..consumed].matches('\n').count() + 1
128}
129
130fn starts_with_node_keyword(s: &str) -> bool {
131 s.starts_with("group")
132 || s.starts_with("frame")
133 || s.starts_with("rect")
134 || s.starts_with("ellipse")
135 || s.starts_with("path")
136 || s.starts_with("text")
137}
138
139fn is_generic_node_start(s: &str) -> bool {
142 let rest = match s.strip_prefix('@') {
143 Some(r) => r,
144 None => return false,
145 };
146 let after_id = rest.trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
147 if after_id.len() == rest.len() {
149 return false;
150 }
151 after_id.trim_start().starts_with('{')
152}
153
154#[derive(Debug)]
156struct ParsedNode {
157 id: NodeId,
158 kind: NodeKind,
159 style: Style,
160 use_styles: Vec<NodeId>,
161 constraints: Vec<Constraint>,
162 animations: Vec<AnimKeyframe>,
163 annotations: Vec<Annotation>,
164 comments: Vec<String>,
166 children: Vec<ParsedNode>,
167 place: Option<(HPlace, VPlace)>,
169}
170
171fn insert_node_recursive(
172 graph: &mut SceneGraph,
173 parent: petgraph::graph::NodeIndex,
174 parsed: ParsedNode,
175) {
176 let mut node = SceneNode::new(parsed.id, parsed.kind);
177 node.style = parsed.style;
178 node.use_styles.extend(parsed.use_styles);
179 node.constraints.extend(parsed.constraints);
180 node.animations.extend(parsed.animations);
181 node.annotations = parsed.annotations;
182 node.comments = parsed.comments;
183 node.place = parsed.place;
184
185 let idx = graph.add_node(parent, node);
186
187 for child in parsed.children {
188 insert_node_recursive(graph, idx, child);
189 }
190}
191
192fn parse_import_line(input: &mut &str) -> ModalResult<Import> {
196 let _ = "import".parse_next(input)?;
197 let _ = space1.parse_next(input)?;
198 let path = parse_quoted_string
199 .map(|s| s.to_string())
200 .parse_next(input)?;
201 let _ = space1.parse_next(input)?;
202 let _ = "as".parse_next(input)?;
203 let _ = space1.parse_next(input)?;
204 let namespace = parse_identifier.map(|s| s.to_string()).parse_next(input)?;
205 skip_opt_separator(input);
206 Ok(Import { path, namespace })
207}
208
209const SECTION_SEPARATORS: &[&str] = &[
214 "─── Themes ───",
215 "─── Layout ───",
216 "─── Constraints ───",
217 "─── Flows ───",
218];
219
220fn is_section_separator(text: &str) -> bool {
222 SECTION_SEPARATORS.iter().any(|sep| text.contains(sep))
223}
224
225fn collect_leading_comments(input: &mut &str) -> Vec<String> {
228 let mut comments = Vec::new();
229 loop {
230 let before = *input;
232 *input = input.trim_start();
233 if input.starts_with('#') {
234 let end = input.find('\n').unwrap_or(input.len());
236 let text = input[1..end].trim().to_string();
237 *input = &input[end.min(input.len())..];
238 if input.starts_with('\n') {
239 *input = &input[1..];
240 }
241 if !text.is_empty() && !is_section_separator(&text) {
243 comments.push(text);
244 }
245 continue;
246 }
247 if *input == before {
248 break;
249 }
250 }
251 comments
252}
253
254fn skip_ws_and_comments(input: &mut &str) {
257 let _ = collect_leading_comments(input);
258}
259
260fn skip_space(input: &mut &str) {
262 use winnow::ascii::space0;
263 let _: Result<&str, winnow::error::ErrMode<ContextError>> = space0.parse_next(input);
264}
265
266fn parse_identifier<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
267 take_while(1.., |c: char| c.is_alphanumeric() || c == '_').parse_next(input)
268}
269
270fn parse_node_id(input: &mut &str) -> ModalResult<NodeId> {
271 preceded('@', parse_identifier)
272 .map(NodeId::intern)
273 .parse_next(input)
274}
275
276fn parse_hex_color(input: &mut &str) -> ModalResult<Color> {
277 let _ = '#'.parse_next(input)?;
278 let hex_digits: &str = take_while(1..=8, |c: char| c.is_ascii_hexdigit()).parse_next(input)?;
279 Color::from_hex(hex_digits)
280 .ok_or_else(|| winnow::error::ErrMode::Backtrack(ContextError::new()))
281}
282
283fn parse_number(input: &mut &str) -> ModalResult<f32> {
284 let start = *input;
285 if input.starts_with('-') {
286 *input = &input[1..];
287 }
288 let _ = take_while(1.., |c: char| c.is_ascii_digit()).parse_next(input)?;
289 if input.starts_with('.') {
290 *input = &input[1..];
291 let _ =
292 take_while::<_, _, ContextError>(0.., |c: char| c.is_ascii_digit()).parse_next(input);
293 }
294 let matched = &start[..start.len() - input.len()];
295 matched
296 .parse::<f32>()
297 .map_err(|_| winnow::error::ErrMode::Backtrack(ContextError::new()))
298}
299
300fn parse_quoted_string<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
301 delimited('"', take_till(0.., '"'), '"').parse_next(input)
302}
303
304fn skip_opt_separator(input: &mut &str) {
305 if input.starts_with(';') || input.starts_with('\n') {
306 *input = &input[1..];
307 }
308}
309
310fn skip_px_suffix(input: &mut &str) {
312 if input.starts_with("px") {
313 *input = &input[2..];
314 }
315}
316
317fn parse_spec_block(input: &mut &str) -> ModalResult<Vec<Annotation>> {
321 let _ = "spec".parse_next(input)?;
322 skip_space(input);
323
324 if input.starts_with('"') {
326 let desc = parse_quoted_string
327 .map(|s| s.to_string())
328 .parse_next(input)?;
329 skip_opt_separator(input);
330 return Ok(vec![Annotation::Description(desc)]);
331 }
332
333 let _ = '{'.parse_next(input)?;
335 let mut annotations = Vec::new();
336 skip_ws_and_comments(input);
337
338 while !input.starts_with('}') {
339 annotations.push(parse_spec_item.parse_next(input)?);
340 skip_ws_and_comments(input);
341 }
342
343 let _ = '}'.parse_next(input)?;
344 Ok(annotations)
345}
346
347fn parse_spec_item(input: &mut &str) -> ModalResult<Annotation> {
356 if input.starts_with('"') {
358 let desc = parse_quoted_string
359 .map(|s| s.to_string())
360 .parse_next(input)?;
361 skip_opt_separator(input);
362 return Ok(Annotation::Description(desc));
363 }
364
365 let keyword = parse_identifier.parse_next(input)?;
367 skip_space(input);
368 let _ = ':'.parse_next(input)?;
369 skip_space(input);
370
371 let value = if input.starts_with('"') {
372 parse_quoted_string
373 .map(|s| s.to_string())
374 .parse_next(input)?
375 } else {
376 let v: &str =
377 take_till(0.., |c: char| c == '\n' || c == ';' || c == '}').parse_next(input)?;
378 v.trim().to_string()
379 };
380
381 let ann = match keyword {
382 "accept" => Annotation::Accept(value),
383 "status" => Annotation::Status(value),
384 "priority" => Annotation::Priority(value),
385 "tag" => Annotation::Tag(value),
386 _ => Annotation::Description(format!("{keyword}: {value}")),
387 };
388
389 skip_opt_separator(input);
390 Ok(ann)
391}
392
393fn parse_style_block(input: &mut &str) -> ModalResult<(NodeId, Style)> {
396 let _ = alt(("theme", "style")).parse_next(input)?;
397 let _ = space1.parse_next(input)?;
398 let name = parse_identifier.map(NodeId::intern).parse_next(input)?;
399 skip_space(input);
400 let _ = '{'.parse_next(input)?;
401
402 let mut style = Style::default();
403 skip_ws_and_comments(input);
404
405 while !input.starts_with('}') {
406 parse_style_property(input, &mut style)?;
407 skip_ws_and_comments(input);
408 }
409
410 let _ = '}'.parse_next(input)?;
411 Ok((name, style))
412}
413
414fn parse_style_property(input: &mut &str, style: &mut Style) -> ModalResult<()> {
415 let prop_name = parse_identifier.parse_next(input)?;
416 skip_space(input);
417 let _ = ':'.parse_next(input)?;
418 skip_space(input);
419
420 match prop_name {
421 "fill" | "background" | "color" => {
422 style.fill = Some(parse_paint(input)?);
423 }
424 "font" => {
425 parse_font_value(input, style)?;
426 }
427 "corner" | "rounded" | "radius" => {
428 style.corner_radius = Some(parse_number.parse_next(input)?);
429 skip_px_suffix(input);
430 }
431 "opacity" => {
432 style.opacity = Some(parse_number.parse_next(input)?);
433 }
434 "align" | "text_align" => {
435 parse_align_value(input, style)?;
436 }
437 _ => {
438 let _ =
439 take_till::<_, _, ContextError>(0.., |c: char| c == '\n' || c == ';' || c == '}')
440 .parse_next(input);
441 }
442 }
443
444 skip_opt_separator(input);
445 Ok(())
446}
447
448fn weight_name_to_number(name: &str) -> Option<u16> {
450 match name {
451 "thin" => Some(100),
452 "extralight" | "extra_light" => Some(200),
453 "light" => Some(300),
454 "regular" | "normal" => Some(400),
455 "medium" => Some(500),
456 "semibold" | "semi_bold" => Some(600),
457 "bold" => Some(700),
458 "extrabold" | "extra_bold" => Some(800),
459 "black" | "heavy" => Some(900),
460 _ => None,
461 }
462}
463
464fn parse_font_value(input: &mut &str, style: &mut Style) -> ModalResult<()> {
465 let mut font = style.font.clone().unwrap_or_default();
466
467 if input.starts_with('"') {
468 let family = parse_quoted_string.parse_next(input)?;
469 font.family = family.to_string();
470 skip_space(input);
471 }
472
473 let saved = *input;
475 if let Ok(name) = parse_identifier.parse_next(input) {
476 if let Some(w) = weight_name_to_number(name) {
477 font.weight = w;
478 skip_space(input);
479 if let Ok(size) = parse_number.parse_next(input) {
480 font.size = size;
481 skip_px_suffix(input);
482 }
483 } else {
484 *input = saved; }
486 }
487
488 if *input == saved
490 && let Ok(n1) = parse_number.parse_next(input)
491 {
492 skip_space(input);
493 if let Ok(n2) = parse_number.parse_next(input) {
494 font.weight = n1 as u16;
495 font.size = n2;
496 skip_px_suffix(input);
497 } else {
498 font.size = n1;
499 skip_px_suffix(input);
500 }
501 }
502
503 style.font = Some(font);
504 Ok(())
505}
506
507fn parse_node(input: &mut &str) -> ModalResult<ParsedNode> {
510 let kind_str = if input.starts_with('@') {
512 "generic"
513 } else {
514 alt((
515 "group".value("group"),
516 "frame".value("frame"),
517 "rect".value("rect"),
518 "ellipse".value("ellipse"),
519 "path".value("path"),
520 "text".value("text"),
521 ))
522 .parse_next(input)?
523 };
524
525 skip_space(input);
526
527 let id = if input.starts_with('@') {
528 parse_node_id.parse_next(input)?
529 } else {
530 NodeId::anonymous(kind_str)
531 };
532
533 skip_space(input);
534
535 let inline_text = if kind_str == "text" && input.starts_with('"') {
536 Some(
537 parse_quoted_string
538 .map(|s| s.to_string())
539 .parse_next(input)?,
540 )
541 } else {
542 None
543 };
544
545 skip_space(input);
546 let _ = '{'.parse_next(input)?;
547
548 let mut style = Style::default();
549 let mut use_styles = Vec::new();
550 let mut constraints = Vec::new();
551 let mut animations = Vec::new();
552 let mut annotations = Vec::new();
553 let mut children = Vec::new();
554 let mut width: Option<f32> = None;
555 let mut height: Option<f32> = None;
556 let mut layout = LayoutMode::Free;
557 let mut clip = false;
558 let mut place: Option<(HPlace, VPlace)> = None;
559
560 skip_ws_and_comments(input);
561
562 while !input.starts_with('}') {
563 if input.starts_with("spec ") || input.starts_with("spec{") {
564 annotations.extend(parse_spec_block.parse_next(input)?);
565 } else if starts_with_child_node(input) {
566 let mut child = parse_node.parse_next(input)?;
567 child.comments = Vec::new(); children.push(child);
571 } else if input.starts_with("when") || input.starts_with("anim") {
572 animations.push(parse_anim_block.parse_next(input)?);
573 } else {
574 parse_node_property(
575 input,
576 &mut style,
577 &mut use_styles,
578 &mut constraints,
579 &mut width,
580 &mut height,
581 &mut layout,
582 &mut clip,
583 &mut place,
584 )?;
585 }
586 let _inner_comments = collect_leading_comments(input);
588 }
589
590 let _ = '}'.parse_next(input)?;
591
592 let kind = match kind_str {
593 "group" => NodeKind::Group, "frame" => NodeKind::Frame {
595 width: width.unwrap_or(200.0),
596 height: height.unwrap_or(200.0),
597 clip,
598 layout,
599 },
600 "rect" => NodeKind::Rect {
601 width: width.unwrap_or(100.0),
602 height: height.unwrap_or(100.0),
603 },
604 "ellipse" => NodeKind::Ellipse {
605 rx: width.unwrap_or(50.0),
606 ry: height.unwrap_or(50.0),
607 },
608 "text" => NodeKind::Text {
609 content: inline_text.unwrap_or_default(),
610 max_width: width,
611 },
612 "path" => NodeKind::Path {
613 commands: Vec::new(),
614 },
615 "generic" => NodeKind::Generic,
616 _ => unreachable!(),
617 };
618
619 Ok(ParsedNode {
620 id,
621 kind,
622 style,
623 use_styles,
624 constraints,
625 animations,
626 annotations,
627 comments: Vec::new(),
628 children,
629 place,
630 })
631}
632
633fn starts_with_child_node(input: &str) -> bool {
636 if is_generic_node_start(input) {
638 return true;
639 }
640 let keywords = &[
641 ("group", 5),
642 ("frame", 5),
643 ("rect", 4),
644 ("ellipse", 7),
645 ("path", 4),
646 ("text", 4),
647 ];
648 for &(keyword, len) in keywords {
649 if input.starts_with(keyword) {
650 if keyword == "text" && input.get(len..).is_some_and(|s| s.starts_with('_')) {
651 continue; }
653 if let Some(after) = input.get(len..)
654 && after.starts_with(|c: char| {
655 c == ' ' || c == '\t' || c == '@' || c == '{' || c == '"'
656 })
657 {
658 return true;
659 }
660 }
661 }
662 false
663}
664
665fn named_color_to_hex(name: &str) -> Option<Color> {
667 match name {
668 "red" => Color::from_hex("#EF4444"),
669 "orange" => Color::from_hex("#F97316"),
670 "amber" | "yellow" => Color::from_hex("#F59E0B"),
671 "lime" => Color::from_hex("#84CC16"),
672 "green" => Color::from_hex("#22C55E"),
673 "teal" => Color::from_hex("#14B8A6"),
674 "cyan" => Color::from_hex("#06B6D4"),
675 "blue" => Color::from_hex("#3B82F6"),
676 "indigo" => Color::from_hex("#6366F1"),
677 "purple" | "violet" => Color::from_hex("#8B5CF6"),
678 "pink" => Color::from_hex("#EC4899"),
679 "rose" => Color::from_hex("#F43F5E"),
680 "white" => Color::from_hex("#FFFFFF"),
681 "black" => Color::from_hex("#000000"),
682 "gray" | "grey" => Color::from_hex("#6B7280"),
683 "slate" => Color::from_hex("#64748B"),
684 _ => None,
685 }
686}
687
688fn parse_paint(input: &mut &str) -> ModalResult<Paint> {
690 if input.starts_with("linear(") {
691 let _ = "linear(".parse_next(input)?;
692 let angle = parse_number.parse_next(input)?;
693 let _ = "deg".parse_next(input)?;
694 let stops = parse_gradient_stops(input)?;
695 let _ = ')'.parse_next(input)?;
696 Ok(Paint::LinearGradient { angle, stops })
697 } else if input.starts_with("radial(") {
698 let _ = "radial(".parse_next(input)?;
699 let stops = parse_gradient_stops(input)?;
700 let _ = ')'.parse_next(input)?;
701 Ok(Paint::RadialGradient { stops })
702 } else if input.starts_with('#') {
703 parse_hex_color.map(Paint::Solid).parse_next(input)
704 } else {
705 let saved = *input;
707 if let Ok(name) = parse_identifier.parse_next(input) {
708 if let Some(color) = named_color_to_hex(name) {
709 return Ok(Paint::Solid(color));
710 }
711 *input = saved;
712 }
713 parse_hex_color.map(Paint::Solid).parse_next(input)
714 }
715}
716
717fn parse_gradient_stops(input: &mut &str) -> ModalResult<Vec<GradientStop>> {
722 let mut stops = Vec::new();
723 loop {
724 skip_space(input);
725 if input.starts_with(',') {
727 let _ = ','.parse_next(input)?;
728 skip_space(input);
729 }
730 if input.is_empty() || input.starts_with(')') {
732 break;
733 }
734 let Ok(color) = parse_hex_color.parse_next(input) else {
736 break;
737 };
738 skip_space(input);
739 let offset = parse_number.parse_next(input)?;
740 stops.push(GradientStop { color, offset });
741 }
742 Ok(stops)
743}
744
745#[allow(clippy::too_many_arguments)]
746fn parse_node_property(
747 input: &mut &str,
748 style: &mut Style,
749 use_styles: &mut Vec<NodeId>,
750 constraints: &mut Vec<Constraint>,
751 width: &mut Option<f32>,
752 height: &mut Option<f32>,
753 layout: &mut LayoutMode,
754 clip: &mut bool,
755 place: &mut Option<(HPlace, VPlace)>,
756) -> ModalResult<()> {
757 let prop_name = parse_identifier.parse_next(input)?;
758 skip_space(input);
759 let _ = ':'.parse_next(input)?;
760 skip_space(input);
761
762 match prop_name {
763 "x" => {
764 let x_val = parse_number.parse_next(input)?;
765 if let Some(Constraint::Position { x, .. }) = constraints
767 .iter_mut()
768 .find(|c| matches!(c, Constraint::Position { .. }))
769 {
770 *x = x_val;
771 } else {
772 constraints.push(Constraint::Position { x: x_val, y: 0.0 });
773 }
774 }
775 "y" => {
776 let y_val = parse_number.parse_next(input)?;
777 if let Some(Constraint::Position { y, .. }) = constraints
778 .iter_mut()
779 .find(|c| matches!(c, Constraint::Position { .. }))
780 {
781 *y = y_val;
782 } else {
783 constraints.push(Constraint::Position { x: 0.0, y: y_val });
784 }
785 }
786 "w" | "width" => {
787 *width = Some(parse_number.parse_next(input)?);
788 skip_px_suffix(input);
789 skip_space(input);
790 if input.starts_with("h:") || input.starts_with("h :") {
791 let _ = "h".parse_next(input)?;
792 skip_space(input);
793 let _ = ':'.parse_next(input)?;
794 skip_space(input);
795 *height = Some(parse_number.parse_next(input)?);
796 skip_px_suffix(input);
797 }
798 }
799 "h" | "height" => {
800 *height = Some(parse_number.parse_next(input)?);
801 skip_px_suffix(input);
802 }
803 "fill" | "background" | "color" => {
804 style.fill = Some(parse_paint(input)?);
805 }
806 "bg" => {
807 style.fill = Some(Paint::Solid(parse_hex_color.parse_next(input)?));
808 loop {
809 skip_space(input);
810 if input.starts_with("corner=") {
811 let _ = "corner=".parse_next(input)?;
812 style.corner_radius = Some(parse_number.parse_next(input)?);
813 } else if input.starts_with("shadow=(") {
814 let _ = "shadow=(".parse_next(input)?;
815 let ox = parse_number.parse_next(input)?;
816 let _ = ','.parse_next(input)?;
817 let oy = parse_number.parse_next(input)?;
818 let _ = ','.parse_next(input)?;
819 let blur = parse_number.parse_next(input)?;
820 let _ = ','.parse_next(input)?;
821 let color = parse_hex_color.parse_next(input)?;
822 let _ = ')'.parse_next(input)?;
823 style.shadow = Some(Shadow {
824 offset_x: ox,
825 offset_y: oy,
826 blur,
827 color,
828 });
829 } else {
830 break;
831 }
832 }
833 }
834 "stroke" => {
835 let color = parse_hex_color.parse_next(input)?;
836 let _ = space1.parse_next(input)?;
837 let w = parse_number.parse_next(input)?;
838 style.stroke = Some(Stroke {
839 paint: Paint::Solid(color),
840 width: w,
841 ..Stroke::default()
842 });
843 }
844 "corner" | "rounded" | "radius" => {
845 style.corner_radius = Some(parse_number.parse_next(input)?);
846 skip_px_suffix(input);
847 }
848 "opacity" => {
849 style.opacity = Some(parse_number.parse_next(input)?);
850 }
851 "align" | "text_align" => {
852 parse_align_value(input, style)?;
853 }
854 "place" => {
855 *place = Some(parse_place_value(input)?);
856 }
857 "shadow" => {
858 skip_space(input);
860 if input.starts_with('(') {
861 let _ = '('.parse_next(input)?;
862 let ox = parse_number.parse_next(input)?;
863 let _ = ','.parse_next(input)?;
864 let oy = parse_number.parse_next(input)?;
865 let _ = ','.parse_next(input)?;
866 let blur = parse_number.parse_next(input)?;
867 let _ = ','.parse_next(input)?;
868 let color = parse_hex_color.parse_next(input)?;
869 let _ = ')'.parse_next(input)?;
870 style.shadow = Some(Shadow {
871 offset_x: ox,
872 offset_y: oy,
873 blur,
874 color,
875 });
876 }
877 }
878 "label" => {
879 if input.starts_with('"') {
882 let _ = parse_quoted_string.parse_next(input)?;
883 } else {
884 let _ = take_till::<_, _, ContextError>(0.., |c: char| {
885 c == '\n' || c == ';' || c == '}'
886 })
887 .parse_next(input);
888 }
889 }
890 "use" => {
891 use_styles.push(parse_identifier.map(NodeId::intern).parse_next(input)?);
892 }
893 "font" => {
894 parse_font_value(input, style)?;
895 }
896 "layout" => {
897 let mode_str = parse_identifier.parse_next(input)?;
898 skip_space(input);
899 let mut gap = 0.0f32;
900 let mut pad = 0.0f32;
901 loop {
902 skip_space(input);
903 if input.starts_with("gap=") {
904 let _ = "gap=".parse_next(input)?;
905 gap = parse_number.parse_next(input)?;
906 } else if input.starts_with("pad=") {
907 let _ = "pad=".parse_next(input)?;
908 pad = parse_number.parse_next(input)?;
909 } else if input.starts_with("cols=") {
910 let _ = "cols=".parse_next(input)?;
911 let _ = parse_number.parse_next(input)?;
912 } else {
913 break;
914 }
915 }
916 *layout = match mode_str {
917 "column" => LayoutMode::Column { gap, pad },
918 "row" => LayoutMode::Row { gap, pad },
919 "grid" => LayoutMode::Grid { cols: 2, gap, pad },
920 _ => LayoutMode::Free,
921 };
922 }
923 "clip" => {
924 let val = parse_identifier.parse_next(input)?;
925 *clip = val == "true";
926 }
927 _ => {
928 let _ =
929 take_till::<_, _, ContextError>(0.., |c: char| c == '\n' || c == ';' || c == '}')
930 .parse_next(input);
931 }
932 }
933
934 skip_opt_separator(input);
935 Ok(())
936}
937
938fn parse_align_value(input: &mut &str, style: &mut Style) -> ModalResult<()> {
941 use crate::model::{TextAlign, TextVAlign};
942
943 let first = parse_identifier.parse_next(input)?;
944 style.text_align = Some(match first {
945 "left" => TextAlign::Left,
946 "right" => TextAlign::Right,
947 _ => TextAlign::Center, });
949
950 skip_space(input);
952 let at_end = input.is_empty()
953 || input.starts_with('\n')
954 || input.starts_with(';')
955 || input.starts_with('}');
956 if !at_end && let Ok(second) = parse_identifier.parse_next(input) {
957 style.text_valign = Some(match second {
958 "top" => TextVAlign::Top,
959 "bottom" => TextVAlign::Bottom,
960 _ => TextVAlign::Middle,
961 });
962 }
963
964 Ok(())
965}
966
967fn parse_place_value(input: &mut &str) -> ModalResult<(HPlace, VPlace)> {
970 use crate::model::{HPlace, VPlace};
971
972 let first = parse_identifier.parse_next(input)?;
973
974 if input.starts_with('-') {
976 let saved = *input;
977 *input = &input[1..]; if let Ok(second) = parse_identifier.parse_next(input) {
979 match (first, second) {
980 ("top", "left") => return Ok((HPlace::Left, VPlace::Top)),
981 ("top", "right") => return Ok((HPlace::Right, VPlace::Top)),
982 ("bottom", "left") => return Ok((HPlace::Left, VPlace::Bottom)),
983 ("bottom", "right") => return Ok((HPlace::Right, VPlace::Bottom)),
984 _ => *input = saved, }
986 } else {
987 *input = saved;
988 }
989 }
990
991 match first {
992 "center" => {
993 skip_space(input);
995 let at_end = input.is_empty()
996 || input.starts_with('\n')
997 || input.starts_with(';')
998 || input.starts_with('}');
999 if !at_end && let Ok(second) = parse_identifier.parse_next(input) {
1000 let v = match second {
1001 "top" => VPlace::Top,
1002 "bottom" => VPlace::Bottom,
1003 _ => VPlace::Middle,
1004 };
1005 return Ok((HPlace::Center, v));
1006 }
1007 Ok((HPlace::Center, VPlace::Middle))
1008 }
1009 "top" => Ok((HPlace::Center, VPlace::Top)),
1010 "bottom" => Ok((HPlace::Center, VPlace::Bottom)),
1011 _ => {
1012 let h = match first {
1014 "left" => HPlace::Left,
1015 "right" => HPlace::Right,
1016 _ => HPlace::Center,
1017 };
1018
1019 skip_space(input);
1020 let at_end = input.is_empty()
1021 || input.starts_with('\n')
1022 || input.starts_with(';')
1023 || input.starts_with('}');
1024 if !at_end && let Ok(second) = parse_identifier.parse_next(input) {
1025 let v = match second {
1026 "top" => VPlace::Top,
1027 "bottom" => VPlace::Bottom,
1028 _ => VPlace::Middle,
1029 };
1030 return Ok((h, v));
1031 }
1032
1033 Ok((h, VPlace::Middle))
1034 }
1035 }
1036}
1037
1038fn parse_anim_block(input: &mut &str) -> ModalResult<AnimKeyframe> {
1041 let _ = alt(("when", "anim")).parse_next(input)?;
1042 let _ = space1.parse_next(input)?;
1043 let _ = ':'.parse_next(input)?;
1044 let trigger_str = parse_identifier.parse_next(input)?;
1045 let trigger = match trigger_str {
1046 "hover" => AnimTrigger::Hover,
1047 "press" => AnimTrigger::Press,
1048 "enter" => AnimTrigger::Enter,
1049 other => AnimTrigger::Custom(other.to_string()),
1050 };
1051
1052 skip_space(input);
1053 let _ = '{'.parse_next(input)?;
1054
1055 let mut props = AnimProperties::default();
1056 let mut duration_ms = 300u32;
1057 let mut easing = Easing::EaseInOut;
1058
1059 skip_ws_and_comments(input);
1060
1061 while !input.starts_with('}') {
1062 let prop = parse_identifier.parse_next(input)?;
1063 skip_space(input);
1064 let _ = ':'.parse_next(input)?;
1065 skip_space(input);
1066
1067 match prop {
1068 "fill" => {
1069 props.fill = Some(Paint::Solid(parse_hex_color.parse_next(input)?));
1070 }
1071 "opacity" => {
1072 props.opacity = Some(parse_number.parse_next(input)?);
1073 }
1074 "scale" => {
1075 props.scale = Some(parse_number.parse_next(input)?);
1076 }
1077 "rotate" => {
1078 props.rotate = Some(parse_number.parse_next(input)?);
1079 }
1080 "ease" => {
1081 let ease_name = parse_identifier.parse_next(input)?;
1082 easing = match ease_name {
1083 "linear" => Easing::Linear,
1084 "ease_in" | "easeIn" => Easing::EaseIn,
1085 "ease_out" | "easeOut" => Easing::EaseOut,
1086 "ease_in_out" | "easeInOut" => Easing::EaseInOut,
1087 "spring" => Easing::Spring,
1088 _ => Easing::EaseInOut,
1089 };
1090 skip_space(input);
1091 if let Ok(n) = parse_number.parse_next(input) {
1092 duration_ms = n as u32;
1093 if input.starts_with("ms") {
1094 *input = &input[2..];
1095 }
1096 }
1097 }
1098 _ => {
1099 let _ = take_till::<_, _, ContextError>(0.., |c: char| {
1100 c == '\n' || c == ';' || c == '}'
1101 })
1102 .parse_next(input);
1103 }
1104 }
1105
1106 skip_opt_separator(input);
1107 skip_ws_and_comments(input);
1108 }
1109
1110 let _ = '}'.parse_next(input)?;
1111
1112 Ok(AnimKeyframe {
1113 trigger,
1114 duration_ms,
1115 easing,
1116 properties: props,
1117 })
1118}
1119
1120fn parse_edge_anchor(input: &mut &str) -> ModalResult<EdgeAnchor> {
1124 skip_space(input);
1125 if input.starts_with('@') {
1126 Ok(EdgeAnchor::Node(parse_node_id.parse_next(input)?))
1127 } else {
1128 let x = parse_number.parse_next(input)?;
1129 skip_space(input);
1130 let y = parse_number.parse_next(input)?;
1131 Ok(EdgeAnchor::Point(x, y))
1132 }
1133}
1134
1135fn parse_edge_block(input: &mut &str) -> ModalResult<(Edge, Option<(NodeId, String)>)> {
1138 let _ = "edge".parse_next(input)?;
1139 let _ = space1.parse_next(input)?;
1140
1141 let id = if input.starts_with('@') {
1142 parse_node_id.parse_next(input)?
1143 } else {
1144 NodeId::anonymous("edge")
1145 };
1146
1147 skip_space(input);
1148 let _ = '{'.parse_next(input)?;
1149
1150 let mut from = None;
1151 let mut to = None;
1152 let mut text_child = None;
1153 let mut text_child_content = None; let mut style = Style::default();
1155 let mut use_styles = Vec::new();
1156 let mut arrow = ArrowKind::None;
1157 let mut curve = CurveKind::Straight;
1158 let mut annotations = Vec::new();
1159 let mut animations = Vec::new();
1160 let mut flow = None;
1161 let mut label_offset = None;
1162
1163 skip_ws_and_comments(input);
1164
1165 while !input.starts_with('}') {
1166 if input.starts_with("spec ") || input.starts_with("spec{") {
1167 annotations.extend(parse_spec_block.parse_next(input)?);
1168 } else if input.starts_with("when") || input.starts_with("anim") {
1169 animations.push(parse_anim_block.parse_next(input)?);
1170 } else if input.starts_with("text ") || input.starts_with("text@") {
1171 let node = parse_node.parse_next(input)?;
1173 if let NodeKind::Text { ref content, .. } = node.kind {
1174 text_child = Some(node.id);
1175 text_child_content = Some((node.id, content.clone()));
1176 }
1177 } else {
1178 let prop = parse_identifier.parse_next(input)?;
1179 skip_space(input);
1180 let _ = ':'.parse_next(input)?;
1181 skip_space(input);
1182
1183 match prop {
1184 "from" => {
1185 from = Some(parse_edge_anchor(input)?);
1186 }
1187 "to" => {
1188 to = Some(parse_edge_anchor(input)?);
1189 }
1190 "label" => {
1191 let s = parse_quoted_string
1193 .map(|s| s.to_string())
1194 .parse_next(input)?;
1195 let label_id = NodeId::intern(&format!("_{}_label", id.as_str()));
1196 text_child = Some(label_id);
1197 text_child_content = Some((label_id, s));
1198 }
1199 "stroke" => {
1200 let color = parse_hex_color.parse_next(input)?;
1201 skip_space(input);
1202 let w = parse_number.parse_next(input).unwrap_or(1.0);
1203 style.stroke = Some(Stroke {
1204 paint: Paint::Solid(color),
1205 width: w,
1206 ..Stroke::default()
1207 });
1208 }
1209 "arrow" => {
1210 let kind = parse_identifier.parse_next(input)?;
1211 arrow = match kind {
1212 "none" => ArrowKind::None,
1213 "start" => ArrowKind::Start,
1214 "end" => ArrowKind::End,
1215 "both" => ArrowKind::Both,
1216 _ => ArrowKind::None,
1217 };
1218 }
1219 "curve" => {
1220 let kind = parse_identifier.parse_next(input)?;
1221 curve = match kind {
1222 "straight" => CurveKind::Straight,
1223 "smooth" => CurveKind::Smooth,
1224 "step" => CurveKind::Step,
1225 _ => CurveKind::Straight,
1226 };
1227 }
1228 "use" => {
1229 use_styles.push(parse_identifier.map(NodeId::intern).parse_next(input)?);
1230 }
1231 "opacity" => {
1232 style.opacity = Some(parse_number.parse_next(input)?);
1233 }
1234 "flow" => {
1235 let kind_str = parse_identifier.parse_next(input)?;
1236 let kind = match kind_str {
1237 "pulse" => FlowKind::Pulse,
1238 "dash" => FlowKind::Dash,
1239 _ => FlowKind::Pulse,
1240 };
1241 skip_space(input);
1242 let dur = parse_number.parse_next(input).unwrap_or(800.0) as u32;
1243 if input.starts_with("ms") {
1244 *input = &input[2..];
1245 }
1246 flow = Some(FlowAnim {
1247 kind,
1248 duration_ms: dur,
1249 });
1250 }
1251 "label_offset" => {
1252 let ox = parse_number.parse_next(input)?;
1253 skip_space(input);
1254 let oy = parse_number.parse_next(input)?;
1255 label_offset = Some((ox, oy));
1256 }
1257 _ => {
1258 let _ = take_till::<_, _, ContextError>(0.., |c: char| {
1259 c == '\n' || c == ';' || c == '}'
1260 })
1261 .parse_next(input);
1262 }
1263 }
1264
1265 skip_opt_separator(input);
1266 }
1267 skip_ws_and_comments(input);
1268 }
1269
1270 let _ = '}'.parse_next(input)?;
1271
1272 if style.stroke.is_none() {
1274 style.stroke = Some(Stroke {
1275 paint: Paint::Solid(Color::rgba(0.42, 0.44, 0.5, 1.0)),
1276 width: 1.5,
1277 ..Stroke::default()
1278 });
1279 }
1280
1281 Ok((
1282 Edge {
1283 id,
1284 from: from.unwrap_or(EdgeAnchor::Point(0.0, 0.0)),
1285 to: to.unwrap_or(EdgeAnchor::Point(0.0, 0.0)),
1286 text_child,
1287 style,
1288 use_styles: use_styles.into(),
1289 arrow,
1290 curve,
1291 annotations,
1292 animations: animations.into(),
1293 flow,
1294 label_offset,
1295 },
1296 text_child_content,
1297 ))
1298}
1299
1300fn parse_constraint_line(input: &mut &str) -> ModalResult<(NodeId, Constraint)> {
1303 let node_id = parse_node_id.parse_next(input)?;
1304 skip_space(input);
1305 let _ = "->".parse_next(input)?;
1306 skip_space(input);
1307
1308 let constraint_type = parse_identifier.parse_next(input)?;
1309 skip_space(input);
1310 let _ = ':'.parse_next(input)?;
1311 skip_space(input);
1312
1313 let constraint = match constraint_type {
1314 "center_in" => Constraint::CenterIn(NodeId::intern(parse_identifier.parse_next(input)?)),
1315 "offset" => {
1316 let from = parse_node_id.parse_next(input)?;
1317 let _ = space1.parse_next(input)?;
1318 let dx = parse_number.parse_next(input)?;
1319 skip_space(input);
1320 let _ = ','.parse_next(input)?;
1321 skip_space(input);
1322 let dy = parse_number.parse_next(input)?;
1323 Constraint::Offset { from, dx, dy }
1324 }
1325 "fill_parent" => {
1326 let pad = opt(parse_number).parse_next(input)?.unwrap_or(0.0);
1327 Constraint::FillParent { pad }
1328 }
1329 "absolute" | "position" => {
1330 let x = parse_number.parse_next(input)?;
1331 skip_space(input);
1332 let _ = ','.parse_next(input)?;
1333 skip_space(input);
1334 let y = parse_number.parse_next(input)?;
1335 Constraint::Position { x, y }
1336 }
1337 _ => {
1338 let _ = take_till::<_, _, ContextError>(0.., '\n').parse_next(input);
1339 Constraint::Position { x: 0.0, y: 0.0 }
1340 }
1341 };
1342
1343 if input.starts_with('\n') {
1344 *input = &input[1..];
1345 }
1346 Ok((node_id, constraint))
1347}
1348
1349#[cfg(test)]
1350#[path = "parser_tests.rs"]
1351mod tests;