maker_panel/
parser.rs

1use crate::Direction;
2use nom::branch::alt;
3use nom::bytes::complete::{tag, tag_no_case, take_while};
4use nom::character::complete::{multispace0, one_of};
5use nom::combinator::{all_consuming, cut, map, opt};
6use nom::error::{context, VerboseError};
7use nom::multi::{fold_many1, many0};
8use nom::sequence::{delimited, tuple};
9use nom::IResult;
10use std::collections::HashMap;
11
12#[derive(Debug, Clone)]
13pub enum Value {
14    Float(f64),
15    Ref(String),
16    Cel(String),
17}
18
19impl Value {
20    fn float(&self) -> f64 {
21        match self {
22            Value::Float(f) => *f,
23            _ => unimplemented!(),
24        }
25    }
26
27    fn rfloat(&self, r: &ResolverContext) -> Result<f64, Err> {
28        match self {
29            Value::Float(f) => Ok(*f),
30            Value::Ref(ident) => match r.definitions.get(ident) {
31                Some(var) => match var {
32                    Variable::Number(n) => Ok(*n),
33                    _ => Err(Err::BadType(ident.to_string())),
34                },
35                None => Err(Err::UndefinedVariable(ident.to_string())),
36            },
37            Value::Cel(exp) => {
38                use cel_interpreter::*;
39                match r.eval_cel(exp.to_string()) {
40                    objects::CelType::UInt(n) => Ok(n as f64),
41                    objects::CelType::Int(n) => Ok(n as f64),
42                    objects::CelType::Float(n) => Ok(n),
43                    _ => Err(Err::BadType(exp.to_string())),
44                }
45            }
46        }
47    }
48}
49
50#[derive(Debug, Clone)]
51enum Variable {
52    Geo(AST),
53    Number(f64),
54}
55
56#[derive(Debug, Clone, Default)]
57struct ResolverContext {
58    pub definitions: HashMap<String, Variable>,
59}
60
61impl ResolverContext {
62    fn handle_assignment(&mut self, var: String, ast: Box<AST>) {
63        match *ast {
64            AST::Cel(exp) => {
65                use cel_interpreter::objects::CelType;
66                match self.eval_cel(exp) {
67                    CelType::UInt(n) => {
68                        self.definitions.insert(var, Variable::Number(n as f64));
69                    }
70                    CelType::Int(n) => {
71                        self.definitions.insert(var, Variable::Number(n as f64));
72                    }
73                    CelType::Float(n) => {
74                        self.definitions.insert(var, Variable::Number(n));
75                    }
76                    _ => panic!(),
77                }
78            }
79            _ => {
80                self.definitions.insert(var, Variable::Geo(*ast));
81            }
82        }
83    }
84
85    fn cel_ctx(&self) -> cel_interpreter::context::Context {
86        use cel_interpreter::*;
87        let mut ctx = context::Context::default();
88        for (ident, val) in &self.definitions {
89            match val {
90                Variable::Geo(_) => {}
91                Variable::Number(n) => {
92                    ctx.add_variable(ident.clone(), objects::CelType::Float(*n));
93                }
94            }
95        }
96
97        ctx
98    }
99
100    fn eval_cel(&self, exp: String) -> cel_interpreter::objects::CelType {
101        use cel_interpreter::*;
102        match Program::compile(&exp) {
103            Ok(p) => {
104                let ctx = self.cel_ctx();
105                p.execute(&ctx)
106            }
107            Err(e) => panic!("{}", e), // Should never panic: we checked while parsing
108        }
109    }
110}
111
112#[derive(Debug, Clone)]
113pub enum Err {
114    Parse(String),
115    UndefinedVariable(String),
116    BadType(String),
117}
118
119#[derive(Debug, Clone)]
120pub enum InnerAST {
121    ScrewHole(Value),
122    Smiley,
123    MechanicalSolderPoint(Option<(Value, Value)>),
124}
125
126impl InnerAST {
127    fn into_inner_feature<'a>(
128        self,
129        _ctx: &mut ResolverContext,
130    ) -> Box<dyn super::features::InnerFeature + 'a> {
131        use super::features::{MechanicalSolderPoint, ScrewHole, Smiley};
132
133        match self {
134            InnerAST::ScrewHole(dia) => Box::new(ScrewHole::with_diameter(dia.float())),
135            InnerAST::Smiley => Box::new(Smiley::default()),
136            InnerAST::MechanicalSolderPoint(sz) => Box::new(match sz {
137                Some((x, y)) => MechanicalSolderPoint::with_size((x.float(), y.float())),
138                None => MechanicalSolderPoint::default(),
139            }),
140        }
141    }
142}
143
144#[derive(Debug, Clone)]
145pub enum WrapPosition {
146    Cardinal {
147        side: Direction,
148        offset: Value,
149        align: crate::Align,
150    },
151    Corner {
152        side: Direction,
153        opposite: bool,
154        align: crate::Align,
155    },
156    Angle {
157        angle: Value,
158        offset: Value,
159    },
160}
161
162impl WrapPosition {
163    fn into_positioning(self, r: &ResolverContext) -> Result<crate::features::Positioning, Err> {
164        match self {
165            WrapPosition::Cardinal {
166                side,
167                offset,
168                align,
169            } => Ok(crate::features::Positioning::Cardinal {
170                side,
171                align,
172                centerline_adjustment: offset.rfloat(r)?,
173            }),
174            WrapPosition::Corner {
175                side,
176                opposite,
177                align,
178            } => Ok(crate::features::Positioning::Corner {
179                side,
180                align,
181                opposite,
182            }),
183            WrapPosition::Angle { angle, offset } => Ok(crate::features::Positioning::Angle {
184                degrees: angle.rfloat(r)?,
185                amount: offset.rfloat(r)?,
186            }),
187        }
188    }
189}
190
191#[derive(Debug, Clone)]
192pub enum AST {
193    Assign(String, Box<AST>),
194    VarRef(String),
195    Comment(String),
196    Cel(String),
197    Rect {
198        coords: Option<(Value, Value)>,
199        size: Option<(Value, Value)>,
200        inner: Option<InnerAST>,
201        rounded: Option<Value>,
202    },
203    Circle {
204        coords: Option<(Value, Value)>,
205        radius: Value,
206        inner: Option<InnerAST>,
207    },
208    Triangle {
209        size: (Value, Value),
210        inner: Option<InnerAST>,
211    },
212    RMount {
213        depth: Value,
214        dir: crate::Direction,
215    },
216    Array {
217        dir: crate::Direction,
218        num: usize,
219        inner: Box<AST>,
220        vscore: bool,
221    },
222    ColumnLayout {
223        coords: Option<(Value, Value)>,
224        align: crate::Align,
225        inners: Vec<Box<AST>>,
226    },
227    Wrap {
228        inner: Box<AST>,
229        features: Vec<(WrapPosition, Box<AST>)>,
230    },
231    Tuple {
232        inners: Vec<Box<AST>>,
233    },
234    Negative {
235        inners: Vec<Box<AST>>,
236    },
237    Rotate {
238        rotation: Value,
239        inners: Vec<Box<AST>>,
240    },
241}
242
243impl AST {
244    fn into_feature<'a>(
245        self,
246        ctx: &mut ResolverContext,
247    ) -> Result<Box<dyn super::Feature + 'a>, Err> {
248        use super::features::{Circle, RMount, Rect, Triangle};
249
250        match self {
251            AST::Rect {
252                coords,
253                size,
254                inner,
255                rounded: _,
256            } => Ok(if let Some(inner) = inner {
257                let r = Rect::with_inner(inner.into_inner_feature(ctx));
258                let (w, h) = if let Some((w, h)) = size {
259                    (w.rfloat(ctx)?, h.rfloat(ctx)?)
260                } else {
261                    (2., 2.)
262                };
263                let r = if let Some((x, y)) = coords {
264                    r.dimensions((x.rfloat(ctx)?, y.rfloat(ctx)?).into(), w, h)
265                } else {
266                    r.dimensions([0., 0.].into(), w, h)
267                };
268                Box::new(r)
269            } else {
270                Box::new(match (coords, size) {
271                    (Some((x, y)), Some((w, h))) => Rect::with_center(
272                        (x.rfloat(ctx)?, y.rfloat(ctx)?).into(),
273                        w.rfloat(ctx)?,
274                        h.rfloat(ctx)?,
275                    ),
276                    (None, Some((w, h))) => {
277                        Rect::with_center([0., 0.].into(), w.rfloat(ctx)?, h.rfloat(ctx)?)
278                    }
279                    (Some((x, y)), None) => {
280                        Rect::with_center((x.rfloat(ctx)?, y.rfloat(ctx)?).into(), 2., 2.)
281                    }
282                    (None, None) => Rect::with_center([-1f64, -1f64].into(), 2., 2.),
283                })
284            }),
285            AST::Circle {
286                coords,
287                radius,
288                inner,
289            } => Ok(match (inner, coords) {
290                (Some(i), Some((x, y))) => Box::new(Circle::with_inner(
291                    i.into_inner_feature(ctx),
292                    (x.rfloat(ctx)?, y.rfloat(ctx)?).into(),
293                    radius.rfloat(ctx)?,
294                )),
295                (Some(i), None) => Box::new(Circle::wrap_with_radius(
296                    i.into_inner_feature(ctx),
297                    radius.rfloat(ctx)?,
298                )),
299                (None, Some((x, y))) => Box::new(Circle::new(
300                    (x.rfloat(ctx)?, y.rfloat(ctx)?).into(),
301                    radius.rfloat(ctx)?,
302                )),
303                (None, None) => Box::new(Circle::with_radius(radius.rfloat(ctx)?)),
304            }),
305            AST::Triangle { size, inner } => Ok(match inner {
306                Some(i) => Box::new(Triangle::with_inner(i.into_inner_feature(ctx)).dimensions(
307                    [0., 0.].into(),
308                    size.0.rfloat(ctx)?,
309                    size.1.rfloat(ctx)?,
310                )),
311                None => Box::new(Triangle::right_angle(
312                    size.0.rfloat(ctx)?,
313                    size.1.rfloat(ctx)?,
314                )),
315            }),
316            AST::RMount { depth, dir } => {
317                Ok(Box::new(RMount::new(depth.rfloat(ctx)?).direction(dir)))
318            }
319            AST::Array {
320                dir,
321                num,
322                inner,
323                vscore,
324            } => Ok(Box::new(
325                crate::features::repeating::Tile::new(inner.into_feature(ctx)?, dir, num)
326                    .v_score(vscore),
327            )),
328            AST::ColumnLayout {
329                align,
330                inners,
331                coords,
332            } => Ok(Box::new({
333                let mut layout = match align {
334                    crate::Align::Start => crate::features::Column::align_left(
335                        inners
336                            .into_iter()
337                            .map(|i| i.into_feature(ctx))
338                            .collect::<Result<Vec<_>, Err>>()?,
339                    ),
340                    crate::Align::Center => crate::features::Column::align_center(
341                        inners
342                            .into_iter()
343                            .map(|i| i.into_feature(ctx))
344                            .collect::<Result<Vec<_>, Err>>()?,
345                    ),
346                    crate::Align::End => crate::features::Column::align_right(
347                        inners
348                            .into_iter()
349                            .map(|i| i.into_feature(ctx))
350                            .collect::<Result<Vec<_>, Err>>()?,
351                    ),
352                };
353                if let Some((x, y)) = coords {
354                    use crate::features::Feature;
355                    layout.translate([x.rfloat(ctx)?, y.rfloat(ctx)?].into());
356                };
357                layout
358            })),
359            AST::Wrap { inner, features } => {
360                let mut pos = crate::features::AtPos::new(inner.into_feature(ctx)?);
361                for (position, feature) in features {
362                    pos.push(feature.into_feature(ctx)?, position.into_positioning(ctx)?);
363                }
364                Ok(Box::new(pos))
365            }
366            AST::Tuple { inners } => {
367                let mut out: Option<Box<dyn super::Feature>> = None;
368                for inner in inners.into_iter() {
369                    out = match out {
370                        None => Some(inner.into_feature(ctx)?),
371                        Some(left) => Some(Box::new({
372                            let mut out = crate::features::AtPos::new(left);
373                            out.push(
374                                inner.into_feature(ctx)?,
375                                crate::features::Positioning::Cardinal {
376                                    side: Direction::Right,
377                                    align: crate::Align::End,
378                                    centerline_adjustment: 0.,
379                                },
380                            );
381                            out
382                        })),
383                    };
384                }
385
386                Ok(out.unwrap())
387            }
388            AST::Negative { inners } => Ok(Box::new(crate::features::Negative::new(
389                inners
390                    .into_iter()
391                    .map(|f| f.into_feature(ctx))
392                    .collect::<Result<Vec<_>, Err>>()?,
393            ))),
394            AST::Rotate { rotation, inners } => Ok(Box::new(crate::features::Rotate::new(
395                rotation.rfloat(ctx)?,
396                inners
397                    .into_iter()
398                    .map(|f| f.into_feature(ctx))
399                    .collect::<Result<Vec<_>, Err>>()?,
400            ))),
401            AST::Assign(_, _) => unreachable!(),
402            AST::Comment(_) => unreachable!(),
403            AST::Cel(_) => unreachable!(),
404            AST::VarRef(ident) => match ctx.definitions.get(&ident) {
405                Some(var) => match var {
406                    Variable::Geo(ast) => ast.clone().into_feature(ctx),
407                    _ => Err(Err::BadType(ident)),
408                },
409                None => Err(Err::UndefinedVariable(ident)),
410            },
411        }
412    }
413}
414
415fn parse_cel(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
416    let (start, _) = multispace0(i)?;
417    let (i, exp) = context(
418        "cel",
419        delimited(tag("!{"), cut(take_while(|c| c != '}')), tag("}")),
420    )(start)?;
421
422    if let Err(_) = cel_interpreter::Program::compile(exp) {
423        return Err(nom::Err::Error(VerboseError {
424            errors: vec![(
425                start,
426                nom::error::VerboseErrorKind::Nom(nom::error::ErrorKind::Satisfy),
427            )],
428        }));
429    }
430
431    Ok((i, AST::Cel(exp.to_string())))
432}
433
434fn parse_ident(i: &str) -> IResult<&str, String, VerboseError<&str>> {
435    let (i, _) = multispace0(i)?;
436    let (i, s) = context(
437        "ident",
438        take_while(|c| {
439            c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
440        }),
441    )(i)?;
442    Ok((i, s.into()))
443}
444
445fn parse_uint(i: &str) -> IResult<&str, usize, VerboseError<&str>> {
446    let (i, _) = multispace0(i)?;
447    let (i, s) = context("uint", take_while(|c| c == '-' || (c >= '0' && c <= '9')))(i)?;
448    Ok((
449        i,
450        s.parse().map_err(|_e| {
451            nom::Err::Error(VerboseError {
452                errors: vec![(
453                    i,
454                    nom::error::VerboseErrorKind::Nom(nom::error::ErrorKind::Digit),
455                )],
456            })
457        })?,
458    ))
459}
460
461fn parse_float(i: &str) -> IResult<&str, Value, VerboseError<&str>> {
462    let (i, _) = multispace0(i)?;
463
464    // Handle referencing variables
465    if let Ok((i, _)) = tag::<_, _, VerboseError<&str>>("$")(i) {
466        let (i, ident) = parse_ident(i)?;
467        return Ok((i, Value::Ref(ident)));
468    }
469
470    // Handle CEL expressions
471    if let Ok((i, ast)) = parse_cel(i) {
472        match ast {
473            AST::Cel(exp) => {
474                return Ok((i, Value::Cel(exp)));
475            }
476            _ => unreachable!(),
477        }
478    }
479
480    let (i, s) = context(
481        "float",
482        take_while(|c| c == '.' || c == '+' || c == '-' || (c >= '0' && c <= '9')),
483    )(i)?;
484
485    Ok((
486        i,
487        Value::Float(s.parse().map_err(|_e| {
488            nom::Err::Error(VerboseError {
489                errors: vec![(
490                    i,
491                    nom::error::VerboseErrorKind::Nom(nom::error::ErrorKind::Digit),
492                )],
493            })
494        })?),
495    ))
496}
497
498fn parse_coords(i: &str) -> IResult<&str, (Value, Value), VerboseError<&str>> {
499    let (i, _) = multispace0(i)?;
500    let (i, _) = tag("(")(i)?;
501    let (i, x) = parse_float(i)?;
502    let (i, _) = multispace0(i)?;
503    let (i, _) = tag(",")(i)?;
504    let (i, y) = parse_float(i)?;
505    let (i, _) = multispace0(i)?;
506    let (i, _) = tag(")")(i)?;
507    Ok((i, (x, y)))
508}
509
510fn parse_inner(i: &str) -> IResult<&str, InnerAST, VerboseError<&str>> {
511    let (i, _) = multispace0(i)?;
512
513    let (i, inner) = delimited(
514        tuple((tag("("), multispace0)),
515        alt((
516            map(tuple((tag("h"), parse_float)), |(_, f)| {
517                InnerAST::ScrewHole(f)
518            }),
519            map(tag("h"), |_| InnerAST::ScrewHole(Value::Float(3.1))),
520            map(tag("smiley"), |_| InnerAST::Smiley),
521            parse_inner_msp,
522        )),
523        tuple((multispace0, tag(")"))),
524    )(i)?;
525
526    Ok((i, inner))
527}
528
529fn parse_inner_msp(i: &str) -> IResult<&str, InnerAST, VerboseError<&str>> {
530    let (i, _) = tag_no_case("msp")(i)?;
531    match context("msp details", parse_details)(i) {
532        Ok((i2, deets)) => {
533            let size = if let Some((x, y)) = deets.size {
534                Some((x, y))
535            } else if deets.extra.len() == 2 {
536                Some((deets.extra[0].clone(), deets.extra[1].clone()))
537            } else if deets.extra.len() == 1 {
538                Some((deets.extra[0].clone(), deets.extra[0].clone()))
539            } else {
540                None
541            };
542
543            Ok((i2, InnerAST::MechanicalSolderPoint(size)))
544        }
545        Err(_) => Ok((i, InnerAST::MechanicalSolderPoint(None))),
546    }
547}
548
549enum DetailFragment {
550    Coord(Value, Value),
551    Size(Value, Value),
552    Radius(Value),
553    Rounding(Value),
554    Extra(Value),
555}
556
557#[derive(Debug, Default, Clone)]
558struct Details {
559    coords: Option<(Value, Value)>,
560    size: Option<(Value, Value)>,
561    radius: Option<Value>,
562    extra: Vec<Value>,
563    inner: Option<InnerAST>,
564    rounded: Option<Value>,
565}
566
567impl Details {
568    fn parse_pos(i: &str) -> IResult<&str, DetailFragment, VerboseError<&str>> {
569        let (i, _) = multispace0(i)?;
570        let (i, _t) = tag("@")(i)?;
571        let (i, c) = cut(parse_coords)(i)?;
572        Ok((i, DetailFragment::Coord(c.0, c.1)))
573    }
574    fn parse_extra(i: &str) -> IResult<&str, DetailFragment, VerboseError<&str>> {
575        let (i, f) = parse_float(i)?;
576        Ok((i, DetailFragment::Extra(f)))
577    }
578    fn parse_size(i: &str) -> IResult<&str, DetailFragment, VerboseError<&str>> {
579        let (i, _) = multispace0(i)?;
580        let (i, (_, _, _, _, c)) = tuple((
581            alt((tag_no_case("size"), tag_no_case("s"))),
582            multispace0,
583            tag("="),
584            multispace0,
585            cut(parse_coords),
586        ))(i)?;
587        Ok((i, DetailFragment::Size(c.0, c.1)))
588    }
589    fn parse_radius(i: &str) -> IResult<&str, DetailFragment, VerboseError<&str>> {
590        let (i, _) = multispace0(i)?;
591        let (i, (_, _, _, _, r)) = tuple((
592            alt((tag_no_case("radius"), tag_no_case("r"))),
593            multispace0,
594            tag("="),
595            multispace0,
596            cut(parse_float),
597        ))(i)?;
598        Ok((i, DetailFragment::Radius(r)))
599    }
600    fn parse_rounding(i: &str) -> IResult<&str, DetailFragment, VerboseError<&str>> {
601        let (i, _) = multispace0(i)?;
602        let (i, (_, _, _, _, r)) = tuple((
603            alt((tag_no_case("round"), tag_no_case("r"))),
604            multispace0,
605            tag("="),
606            multispace0,
607            cut(parse_float),
608        ))(i)?;
609        Ok((i, DetailFragment::Rounding(r)))
610    }
611
612    fn with_inner(mut self, inner: Option<InnerAST>) -> Self {
613        self.inner = inner;
614        self
615    }
616}
617
618fn parse_details(i: &str) -> IResult<&str, Details, VerboseError<&str>> {
619    let (i, _) = multispace0(i)?;
620
621    let (i, deets) = delimited(
622        tuple((tag("<"), multispace0)),
623        cut(fold_many1(
624            alt((
625                tuple((
626                    context("pos", Details::parse_pos),
627                    multispace0,
628                    opt(tag(",")),
629                )),
630                tuple((
631                    context("size", Details::parse_size),
632                    multispace0,
633                    opt(tag(",")),
634                )),
635                tuple((
636                    context("radius", Details::parse_radius),
637                    multispace0,
638                    opt(tag(",")),
639                )),
640                tuple((
641                    context("rounding", Details::parse_rounding),
642                    multispace0,
643                    opt(tag(",")),
644                )),
645                tuple((Details::parse_extra, multispace0, opt(tag(",")))),
646            )),
647            Details::default(),
648            |mut acc: Details, (fragment, _, _)| {
649                match fragment {
650                    DetailFragment::Coord(x, y) => {
651                        acc.coords = Some((x, y));
652                    }
653                    DetailFragment::Size(x, y) => {
654                        acc.size = Some((x, y));
655                    }
656                    DetailFragment::Radius(r) => {
657                        acc.radius = Some(r);
658                    }
659                    DetailFragment::Rounding(r) => {
660                        acc.rounded = Some(r);
661                    }
662                    DetailFragment::Extra(f) => acc.extra.push(f),
663                }
664                acc
665            },
666        )),
667        tuple((tag(">"), multispace0)),
668    )(i)?;
669
670    let (i, inner) = opt(parse_inner)(i)?;
671    Ok((i, deets.with_inner(inner)))
672}
673
674fn parse_rect(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
675    let (i, _) = multispace0(i)?;
676    let (i, _) = tag_no_case("R")(i)?;
677    let (i, deets) = context("rectangle details", parse_details)(i)?;
678
679    let size = if let Some((x, y)) = deets.size {
680        Some((x, y))
681    } else if deets.extra.len() == 2 {
682        Some((deets.extra[0].clone(), deets.extra[1].clone()))
683    } else if deets.extra.len() == 1 {
684        Some((deets.extra[0].clone(), deets.extra[0].clone()))
685    } else {
686        None
687    };
688
689    Ok((
690        i,
691        AST::Rect {
692            size,
693            coords: deets.coords,
694            inner: deets.inner,
695            rounded: deets.rounded,
696        },
697    ))
698}
699
700fn parse_circle(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
701    let (i, _) = multispace0(i)?;
702    let (i, _) = tag_no_case("C")(i)?;
703    let (i2, deets) = context("circle details", parse_details)(i)?;
704
705    let r = if let Some(r) = deets.radius {
706        r
707    } else if deets.extra.len() == 1 {
708        deets.extra[0].clone()
709    } else {
710        return Err(nom::Err::Failure(nom::error::make_error(
711            i,
712            nom::error::ErrorKind::Satisfy,
713        )));
714    };
715
716    Ok((
717        i2,
718        AST::Circle {
719            coords: deets.coords,
720            radius: r,
721            inner: deets.inner,
722        },
723    ))
724}
725
726fn parse_triangle(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
727    let (i, _) = multispace0(i)?;
728    let (i, _) = tag_no_case("T")(i)?;
729    let (i2, deets) = context("triangle details", cut(parse_details))(i)?;
730
731    let size = if let Some((x, y)) = deets.size {
732        (x, y)
733    } else if deets.extra.len() == 2 {
734        (deets.extra[0].clone(), deets.extra[1].clone())
735    } else if deets.extra.len() == 1 {
736        (deets.extra[0].clone(), deets.extra[0].clone())
737    } else {
738        return Err(nom::Err::Failure(nom::error::make_error(
739            i,
740            nom::error::ErrorKind::Satisfy,
741        )));
742    };
743
744    Ok((
745        i2,
746        AST::Triangle {
747            size,
748            inner: deets.inner,
749        },
750    ))
751}
752
753fn parse_rmount(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
754    let (i, _) = multispace0(i)?;
755    let (i, dir) = alt((
756        tag_no_case("mount_cut_left"),
757        tag_no_case("mount_cut_right"),
758        tag_no_case("mount_cut_down"),
759        tag_no_case("mount_cut"),
760    ))(i)?;
761    let (i, deets) = context("mount details", cut(parse_details))(i)?;
762
763    let depth = if deets.extra.len() == 1 {
764        deets.extra[0].clone()
765    } else {
766        return Err(nom::Err::Failure(nom::error::make_error(
767            i,
768            nom::error::ErrorKind::Satisfy,
769        )));
770    };
771
772    Ok((
773        i,
774        AST::RMount {
775            depth,
776            dir: match dir.to_lowercase().as_str() {
777                "mount_cut_left" => crate::Direction::Left,
778                "mount_cut_right" => crate::Direction::Right,
779                "mount_cut_down" => crate::Direction::Down,
780                _ => crate::Direction::Up,
781            },
782        },
783    ))
784}
785
786fn parse_array(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
787    let (i, _) = multispace0(i)?;
788
789    let (i, params) = context(
790        "array",
791        delimited(
792            tuple((tag("["), multispace0)),
793            cut(tuple((
794                parse_uint,
795                opt(tuple((multispace0, tag(";"), multispace0, one_of("UDRL")))),
796                opt(tuple((
797                    multispace0,
798                    tag(";"),
799                    multispace0,
800                    alt((tag_no_case("vscore"), tag_no_case("v-score"))),
801                ))),
802            ))),
803            tuple((tag("]"), multispace0)),
804        ),
805    )(i)?;
806    let (i, geo) = parse_geo(i)?;
807
808    let (num, dir, vscore) = params;
809    let dir = if let Some((_, _, _, s)) = dir {
810        match s {
811            'L' => crate::Direction::Left,
812            'R' => crate::Direction::Right,
813            'U' => crate::Direction::Up,
814            'D' => crate::Direction::Down,
815            _ => {
816                return Err(nom::Err::Failure(nom::error::make_error(
817                    i,
818                    nom::error::ErrorKind::Satisfy,
819                )));
820            }
821        }
822    } else {
823        crate::Direction::Right
824    };
825
826    Ok((
827        i,
828        AST::Array {
829            dir,
830            num,
831            inner: Box::new(geo),
832            vscore: vscore.is_some(),
833        },
834    ))
835}
836
837fn parse_column_layout(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
838    let (i, _) = multispace0(i)?;
839
840    let (i, (dir, _, pos, _, _, inners)) = context(
841        "column",
842        delimited(
843            tuple((tag_no_case("column"), multispace0)),
844            tuple((
845                alt((
846                    tag_no_case("left"),
847                    tag_no_case("center"),
848                    tag_no_case("right"),
849                )),
850                multispace0,
851                opt(tuple((tag("@"), parse_coords))),
852                multispace0,
853                tag("{"),
854                fold_many1(
855                    tuple((parse_geo, multispace0, opt(tag(",")))),
856                    Vec::new(),
857                    |mut acc, (inner, _, _)| {
858                        acc.push(Box::new(inner));
859                        acc
860                    },
861                ),
862            )),
863            tuple((tag("}"), multispace0)),
864        ),
865    )(i)?;
866
867    Ok((
868        i,
869        AST::ColumnLayout {
870            align: match dir.to_lowercase().as_str() {
871                "left" => crate::Align::Start,
872                "right" => crate::Align::End,
873                _ => crate::Align::Center,
874            },
875            inners: inners,
876            coords: pos.map(|x| x.1),
877        },
878    ))
879}
880
881fn parse_pos_spec(i: &str) -> IResult<&str, WrapPosition, VerboseError<&str>> {
882    let (i, (_, side, offset, _, align, _)) = tuple((
883        multispace0,
884        alt((
885            tag_no_case("left"),
886            tag_no_case("right"),
887            tag_no_case("up"),
888            tag_no_case("down"),
889            tag_no_case("top"),
890            tag_no_case("bottom"),
891        )),
892        opt(parse_float),
893        multispace0,
894        opt(tuple((
895            multispace0,
896            tag_no_case("align"),
897            multispace0,
898            alt((
899                tag_no_case("center"),
900                tag_no_case("exterior"),
901                tag_no_case("interior"),
902            )),
903            multispace0,
904        ))),
905        tag("=>"),
906    ))(i)?;
907
908    Ok((
909        i,
910        WrapPosition::Cardinal {
911            side: match side.to_lowercase().as_str() {
912                "left" => Direction::Left,
913                "right" => Direction::Right,
914                "top" | "up" => Direction::Up,
915                "bottom" | "down" => Direction::Down,
916                _ => unreachable!(),
917            },
918            offset: offset.unwrap_or(Value::Float(0.0)),
919            align: match align {
920                Some((_, _, _, align, _)) => match align.to_lowercase().as_str() {
921                    "exterior" => crate::Align::End,
922                    "interior" => crate::Align::Start,
923                    _ => crate::Align::Center,
924                },
925                _ => crate::Align::Center,
926            },
927        },
928    ))
929}
930
931fn parse_about_spec(i: &str) -> IResult<&str, WrapPosition, VerboseError<&str>> {
932    let (i, (_, angle, _, offset, _, _)) = tuple((
933        tuple((multispace0, tag_no_case("angle("))),
934        parse_float,
935        tuple((multispace0, tag(")"))),
936        opt(parse_float),
937        multispace0,
938        tag("=>"),
939    ))(i)?;
940
941    Ok((
942        i,
943        WrapPosition::Angle {
944            angle,
945            offset: offset.unwrap_or(Value::Float(0.0)),
946        },
947    ))
948}
949
950fn parse_wrap_center_spec(i: &str) -> IResult<&str, WrapPosition, VerboseError<&str>> {
951    let (i, _) = tuple((
952        tuple((multispace0, tag_no_case("center"))),
953        multispace0,
954        tag("=>"),
955    ))(i)?;
956
957    Ok((
958        i,
959        WrapPosition::Angle {
960            angle: Value::Float(0.0),
961            offset: Value::Float(0.0),
962        },
963    ))
964}
965
966fn parse_corner_spec(i: &str) -> IResult<&str, WrapPosition, VerboseError<&str>> {
967    let (i, (_, opp, side, _, align, _)) = tuple((
968        multispace0,
969        alt((tag_no_case("min-"), tag_no_case("max-"))),
970        alt((
971            tag_no_case("left"),
972            tag_no_case("right"),
973            tag_no_case("up"),
974            tag_no_case("down"),
975            tag_no_case("top"),
976            tag_no_case("bottom"),
977        )),
978        multispace0,
979        opt(tuple((
980            multispace0,
981            tag_no_case("align"),
982            multispace0,
983            alt((
984                tag_no_case("center"),
985                tag_no_case("exterior"),
986                tag_no_case("interior"),
987            )),
988            multispace0,
989        ))),
990        tag("=>"),
991    ))(i)?;
992
993    Ok((
994        i,
995        WrapPosition::Corner {
996            side: match side.to_lowercase().as_str() {
997                "left" => Direction::Left,
998                "right" => Direction::Right,
999                "top" | "up" => Direction::Up,
1000                "bottom" | "down" => Direction::Down,
1001                _ => unreachable!(),
1002            },
1003            opposite: match opp.to_lowercase().as_str() {
1004                "min-" => false,
1005                "max-" => true,
1006                _ => unreachable!(),
1007            },
1008            align: match align {
1009                Some((_, _, _, align, _)) => match align.to_lowercase().as_str() {
1010                    "exterior" => crate::Align::End,
1011                    "interior" => crate::Align::Start,
1012                    _ => crate::Align::Center,
1013                },
1014                _ => crate::Align::Center,
1015            },
1016        },
1017    ))
1018}
1019
1020fn parse_wrap(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
1021    let (i, _) = multispace0(i)?;
1022
1023    let (i, (_, _, _, inner, _, _, _, _, _, _)) = context(
1024        "wrap",
1025        tuple((
1026            tag_no_case("wrap"),
1027            multispace0,
1028            tag("("),
1029            parse_geo,
1030            multispace0,
1031            tag(")"),
1032            multispace0,
1033            tag_no_case("with"),
1034            multispace0,
1035            tag("{"),
1036        )),
1037    )(i)?;
1038    let (i, elements) = fold_many1(
1039        context(
1040            "pos spec",
1041            alt((
1042                nom::combinator::map(
1043                    tuple((
1044                        alt((
1045                            parse_pos_spec,
1046                            parse_about_spec,
1047                            parse_wrap_center_spec,
1048                            parse_corner_spec,
1049                        )),
1050                        multispace0,
1051                        parse_geo,
1052                        multispace0,
1053                        opt(tag(",")),
1054                    )),
1055                    |s| Some(s),
1056                ),
1057                nom::combinator::map(parse_comment, |_| None),
1058            )),
1059        ),
1060        Vec::new(),
1061        |mut acc, feature| {
1062            if let Some((pos, _, feature, _, _)) = feature {
1063                acc.push((pos, Box::new(feature)));
1064            }
1065            acc
1066        },
1067    )(i)?;
1068    let (i, _) = tuple((multispace0, tag("}"), multispace0))(i)?;
1069
1070    Ok((
1071        i,
1072        AST::Wrap {
1073            inner: Box::new(inner),
1074            features: elements,
1075        },
1076    ))
1077}
1078
1079fn parse_assign(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
1080    let (i, _) = multispace0(i)?;
1081
1082    let (i, (_, _, var, _, _, geo, _, _)) = tuple((
1083        tag("let"),
1084        multispace0,
1085        parse_ident,
1086        multispace0,
1087        tag("="),
1088        parse_geo,
1089        multispace0,
1090        opt(tag(";")),
1091    ))(i)?;
1092
1093    Ok((i, AST::Assign(var, Box::new(geo))))
1094}
1095
1096fn parse_var(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
1097    let (i, _) = multispace0(i)?;
1098    let (i, (_, var)) = tuple((tag("$"), parse_ident))(i)?;
1099    Ok((i, AST::VarRef(var)))
1100}
1101
1102pub fn parse_comment(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
1103    let (i, _) = multispace0(i)?;
1104    let (i, _) = alt((tag("#"), tag("//")))(i)?;
1105    let (i, v) = take_while(|chr| chr != '\n')(i)?;
1106    Ok((i, AST::Comment(v.to_string())))
1107}
1108
1109fn parse_tuple(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
1110    let (i, _) = multispace0(i)?;
1111    let (i, _) = tag("(")(i)?;
1112
1113    let (i, elements) = fold_many1(
1114        tuple((multispace0, parse_geo, multispace0, opt(tag(",")))),
1115        Vec::new(),
1116        |mut acc, (_, feature, _, _)| {
1117            acc.push(Box::new(feature));
1118            acc
1119        },
1120    )(i)?;
1121    let (i, _) = tuple((multispace0, tag(")"), multispace0))(i)?;
1122
1123    Ok((i, AST::Tuple { inners: elements }))
1124}
1125
1126fn parse_negative(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
1127    let (i, _) = multispace0(i)?;
1128
1129    let (i, (_, _, inners)) = context(
1130        "negative",
1131        delimited(
1132            tuple((tag_no_case("negative"), multispace0)),
1133            tuple((
1134                multispace0,
1135                tag("{"),
1136                fold_many1(
1137                    tuple((parse_geo, multispace0, opt(tag(",")))),
1138                    Vec::new(),
1139                    |mut acc, (inner, _, _)| {
1140                        acc.push(Box::new(inner));
1141                        acc
1142                    },
1143                ),
1144            )),
1145            tuple((tag("}"), multispace0)),
1146        ),
1147    )(i)?;
1148
1149    Ok((i, AST::Negative { inners: inners }))
1150}
1151
1152fn parse_rotate(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
1153    let (i, _) = multispace0(i)?;
1154
1155    let (i, (_, _, _, rotation, _, _, _)) = context(
1156        "rotate",
1157        tuple((
1158            tag_no_case("rotate"),
1159            multispace0,
1160            tag("("),
1161            parse_float,
1162            multispace0,
1163            tag(")"),
1164            multispace0,
1165        )),
1166    )(i)?;
1167
1168    let (i, (_, inners)) = context(
1169        "rotate_body",
1170        delimited(
1171            tag("{"),
1172            tuple((
1173                multispace0,
1174                fold_many1(
1175                    tuple((parse_geo, multispace0, opt(tag(",")))),
1176                    Vec::new(),
1177                    |mut acc, (inner, _, _)| {
1178                        acc.push(Box::new(inner));
1179                        acc
1180                    },
1181                ),
1182            )),
1183            tuple((tag("}"), multispace0)),
1184        ),
1185    )(i)?;
1186
1187    Ok((
1188        i,
1189        AST::Rotate {
1190            rotation: rotation,
1191            inners: inners,
1192        },
1193    ))
1194}
1195
1196fn parse_geo(i: &str) -> IResult<&str, AST, VerboseError<&str>> {
1197    alt((
1198        parse_assign,
1199        parse_cel,
1200        parse_array,
1201        parse_rect,
1202        parse_circle,
1203        parse_triangle,
1204        parse_rmount,
1205        parse_wrap,
1206        parse_column_layout,
1207        parse_var,
1208        parse_tuple,
1209        parse_negative,
1210        parse_rotate,
1211        parse_comment,
1212    ))(i)
1213}
1214
1215/// Parses the provided panel spec and returns the series of features
1216/// it represents.
1217pub fn build<'a>(i: &str) -> Result<Vec<Box<dyn super::Feature + 'a>>, Err> {
1218    let mut ctx = ResolverContext::default();
1219    let (_, (g, _)) = all_consuming(tuple((many0(parse_geo), multispace0)))(i).map_err(|e| {
1220        Err::Parse(nom::error::convert_error(
1221            i,
1222            match e {
1223                nom::Err::Error(e) | nom::Err::Failure(e) => e,
1224                _ => unreachable!(),
1225            },
1226        ))
1227    })?;
1228
1229    g.into_iter()
1230        .map(|g| match g {
1231            AST::Assign(var, geo) => {
1232                ctx.handle_assignment(var, geo);
1233                None
1234            }
1235            AST::Comment(_) => None,
1236            _ => Some(g.into_feature(&mut ctx)),
1237        })
1238        .filter(|f| f.is_some())
1239        .map(|f| f.unwrap())
1240        .collect()
1241}
1242
1243#[cfg(test)]
1244mod tests {
1245    use super::*;
1246
1247    #[test]
1248    fn test_rect() {
1249        let out = parse_geo("R<@(1,2)>");
1250        assert!(
1251            matches!(out, Ok(("", AST::Rect{ coords: Some((Value::Float(x), Value::Float(y))), size: None, inner: _, rounded: None })) if
1252                x > 0.99 && x < 1.01 && y > 1.99 && y < 2.01
1253            )
1254        );
1255
1256        let out = parse_geo("R<@(1,2), 2, 4>");
1257        assert!(
1258            matches!(out, Ok(("", AST::Rect{ coords: Some((Value::Float(x), Value::Float(y))), size: Some((Value::Float(w), Value::Float(h))), inner: _, rounded: None })) if
1259                x > 0.99 && x < 1.01 && y > 1.99 && y < 2.01 &&
1260                w > 1.99 && w < 2.01 && h > 3.99 && h < 4.01
1261            )
1262        );
1263
1264        let out = parse_geo("R<@(1,2), 4>");
1265        assert!(
1266            matches!(out, Ok(("", AST::Rect{ coords: Some((Value::Float(x), Value::Float(y))), size: Some((Value::Float(w), Value::Float(h))), inner: _, rounded: None })) if
1267                x > 0.99 && x < 1.01 && y > 1.99 && y < 2.01 &&
1268                w > 3.99 && w < 4.01 && h > 3.99 && h < 4.01
1269            )
1270        );
1271
1272        let out = parse_geo("R<4>");
1273        assert!(
1274            matches!(out, Ok(("", AST::Rect{ coords: None, size: Some((Value::Float(w), Value::Float(h))), inner: _, rounded: None })) if
1275                w > 3.99 && w < 4.01 && h > 3.99 && h < 4.01
1276            )
1277        );
1278
1279        let out = parse_geo("R<@(1,2), size = (2,4)>");
1280        assert!(
1281            matches!(out, Ok(("", AST::Rect{ coords: Some((Value::Float(x), Value::Float(y))), size: Some((Value::Float(w), Value::Float(h))), inner: _, rounded: None })) if
1282                x > 0.99 && x < 1.01 && y > 1.99 && y < 2.01 &&
1283                w > 1.99 && w < 2.01 && h > 3.99 && h < 4.01
1284            )
1285        );
1286
1287        let out = parse_geo(" R<6>(h)");
1288        assert!(
1289            matches!(out, Ok(("", AST::Rect{ coords: None, size: Some((Value::Float(w), Value::Float(h))), inner: Some(InnerAST::ScrewHole(Value::Float(dia))), rounded: None })) if
1290                w > 5.99 && w < 6.01 && h > 5.99 && h < 6.01 &&
1291                dia < 3.11 && dia > 3.09
1292            )
1293        );
1294
1295        let out = parse_geo(" R<6, round = 2>(h)");
1296        assert!(
1297            matches!(out, Ok(("", AST::Rect{ coords: None, size: Some((Value::Float(w), Value::Float(h))), inner: Some(InnerAST::ScrewHole(Value::Float(dia))), rounded: Some(_) })) if
1298                w > 5.99 && w < 6.01 && h > 5.99 && h < 6.01 &&
1299                dia < 3.11 && dia > 3.09
1300            )
1301        );
1302    }
1303
1304    #[test]
1305    fn test_circle() {
1306        let out = parse_geo("C < @ ( 2 , 1 ), 4.5>");
1307        assert!(
1308            matches!(out, Ok(("", AST::Circle{ coords: Some((Value::Float(x), Value::Float(y))), radius: r, inner: _ })) if
1309                y > 0.99 && y < 1.01 && x > 1.99 && x < 2.01 &&
1310                r.float() > 4.49 && r.float() < 4.51
1311            )
1312        );
1313
1314        let out = parse_geo("C<@(2, 1), 3.5>");
1315        assert!(
1316            matches!(out, Ok(("", AST::Circle{ coords: Some((Value::Float(x), Value::Float(y))), radius: r, inner: _ })) if
1317                y > 0.99 && y < 1.01 && x > 1.99 && x < 2.01 &&
1318                r.float() > 3.49 && r.float() < 3.51
1319            )
1320        );
1321        let out = parse_geo("C<3.5>");
1322        assert!(
1323            matches!(out, Ok(("", AST::Circle{ coords: None, radius: r, inner: _ })) if
1324                r.float() > 3.49 && r.float() < 3.51
1325            )
1326        );
1327
1328        let out = parse_geo("C<@(2, 1), R=3.5>");
1329        assert!(
1330            matches!(out, Ok(("", AST::Circle{ coords: Some((Value::Float(x), Value::Float(y))), radius: r, inner: _ })) if
1331                y > 0.99 && y < 1.01 && x> 1.99 && x < 2.01 &&
1332                r.float() > 3.49 && r.float() < 3.51
1333            )
1334        );
1335
1336        let out = parse_geo("C<3.5> ( h9 )");
1337        assert!(
1338            matches!(out, Ok(("", AST::Circle{ coords: None, radius: r, inner: Some(InnerAST::ScrewHole(Value::Float(dia))) })) if
1339                r.float() > 3.49 && r.float() < 3.51 && dia > 8.999 && dia < 9.001
1340            )
1341        );
1342    }
1343
1344    #[test]
1345    fn test_msp() {
1346        let out = parse_geo("C<5>(msp)");
1347        assert!(matches!(
1348            out,
1349            Ok((
1350                "",
1351                AST::Circle {
1352                    inner: Some(InnerAST::MechanicalSolderPoint(None)),
1353                    ..
1354                },
1355            ))
1356        ));
1357
1358        let out = parse_geo("C<5>(msp<2,1>)");
1359        assert!(matches!(
1360            out,
1361            Ok((
1362                "",
1363                AST::Circle { inner: Some(InnerAST::MechanicalSolderPoint(Some((Value::Float(w), Value::Float(h))))), .. },
1364            )) if w > 1.99 && w < 2.01 && h > 0.99 && h < 1.01
1365        ));
1366    }
1367
1368    #[test]
1369    fn test_triangle() {
1370        let out = parse_geo("T<2,1>");
1371        assert!(
1372            matches!(out, Ok(("", AST::Triangle{ size: (Value::Float(x), Value::Float(y)), inner: _ })) if
1373                y > 0.99 && y < 1.01 && x > 1.99 && x < 2.01
1374            )
1375        );
1376    }
1377
1378    #[test]
1379    fn test_r_mount() {
1380        let out = parse_geo("mount_cut<12>");
1381        assert!(matches!(out, Ok(("", AST::RMount{ depth, dir })) if
1382            depth.float() > 11.99 && depth.float() < 12.01 && dir == crate::Direction::Up
1383        ));
1384    }
1385
1386    #[test]
1387    fn test_array() {
1388        let out = parse_geo("[5]C<4.5>");
1389        assert!(
1390            matches!(out, Ok(("", AST::Array{ num: 5, inner: b, dir: crate::Direction::Right, vscore: false})) if
1391                matches!(&*b, AST::Circle{ radius, .. } if radius.float() > 4.4 && radius.float() < 4.6)
1392            )
1393        );
1394
1395        let out = parse_geo("[5; D; v-score]C<4.5>");
1396        assert!(
1397            matches!(out, Ok(("", AST::Array{ num: 5, inner: b, dir: crate::Direction::Down, vscore: true})) if
1398                matches!(&*b, AST::Circle{ radius, .. } if radius.float() > 4.4 && radius.float() < 4.6)
1399            )
1400        );
1401    }
1402
1403    #[test]
1404    fn test_column_layout() {
1405        let out = parse_geo("column left { R<5> }");
1406        assert!(matches!(
1407            out,
1408            Ok((
1409                "",
1410                AST::ColumnLayout {
1411                    align: crate::Align::Start,
1412                    inners: i,
1413                    coords: None,
1414                },
1415            ))
1416            if i.len() == 1
1417        ));
1418
1419        let out = parse_geo("column RiGHt { C<5> R<1> }");
1420        assert!(matches!(
1421            out,
1422            Ok((
1423                "",
1424                AST::ColumnLayout {
1425                    align: crate::Align::End,
1426                    inners: i,
1427                    coords: None,
1428                },
1429            ))
1430            if i.len() == 2
1431        ));
1432
1433        let out = parse_geo("column center { R<1> }");
1434        // eprintln!("{:?}", out);
1435        assert!(matches!(
1436            out,
1437            Ok((
1438                "",
1439                AST::ColumnLayout {
1440                    align: crate::Align::Center,
1441                    inners: i,
1442                    coords: None,
1443                },
1444            ))
1445            if i.len() == 1
1446        ));
1447
1448        let out = parse_geo("column center { (R<1>) }");
1449        // eprintln!("{:?}", out);
1450        assert!(matches!(
1451            out,
1452            Ok((
1453                "",
1454                AST::ColumnLayout {
1455                    align: crate::Align::Center,
1456                    inners: i,
1457                    coords: None,
1458                },
1459            ))
1460            if i.len() == 1 && matches!(*i[0], AST::Tuple{ .. })
1461        ));
1462
1463        let out = parse_geo("column center @(1, 2) { R<1> }");
1464        eprintln!("{:?}", out);
1465        assert!(matches!(
1466            out,
1467            Ok((
1468                "",
1469                AST::ColumnLayout {
1470                    align: crate::Align::Center,
1471                    inners: i,
1472                    coords: Some((Value::Float(x), Value::Float(y))),
1473                },
1474            ))
1475            if i.len() == 1 && x > 0.99 && x < 1.01 && y > 1.99 && y < 2.01
1476        ));
1477    }
1478
1479    #[test]
1480    fn test_wrap() {
1481        let out = parse_geo(
1482            "wrap ($inner) with { left-0.5 => C<2>(h), right align exterior => C<2>(h4) }",
1483        );
1484        // eprintln!("{:?}", out);
1485        assert!(matches!(out, Ok(("", AST::Wrap { inner, features })) if
1486            matches!(*inner, AST::VarRef(ref var) if var == "inner") && features.len() == 2 &&
1487            matches!(features[1].0, WrapPosition::Cardinal{ align: crate::Align::End, .. })
1488        ));
1489
1490        let out = parse_geo(
1491            "wrap(column center {[12] R<5>(h)}) with {left-0.5 => C<2>(h), right+0.5 => C<2>(h)}",
1492        );
1493        // eprintln!("{:?}", out);
1494        assert!(matches!(out, Ok(("", AST::Wrap { inner, features })) if
1495            matches!(*inner, AST::ColumnLayout{ .. }) && features.len() == 2 &&
1496            matches!(features[0].0, WrapPosition::Cardinal{ side: Direction::Left, offset: Value::Float(o1), .. } if
1497            o1 < -0.4 && o1 > -0.6) &&
1498            matches!(features[1].0, WrapPosition::Cardinal{ side: Direction::Right, offset: Value::Float(o2), .. } if
1499           o2 > 0.4 && o2 < 0.6)
1500        ));
1501
1502        let out = parse_geo(
1503            "wrap ($inner) with {\n  left => (C<2>(h), C<2>),\n # test comment\n right => (C<2>(h), C<2>),\n}",
1504        );
1505        // eprintln!("{:?}", out);
1506        assert!(matches!(out, Ok(("", AST::Wrap { inner, features })) if
1507            matches!(*inner, AST::VarRef(ref var) if var == "inner") && features.len() == 2 &&
1508            matches!(features[0].0, WrapPosition::Cardinal{ align: crate::Align::Center, .. })
1509        ));
1510
1511        let out =
1512            parse_geo("wrap ($inner) with {\n  angle(90) => C<2>,\n  angle(-90) 25 => C<2>,\n}");
1513        // eprintln!("{:?}", out);
1514        assert!(matches!(out, Ok(("", AST::Wrap { inner, features })) if
1515            matches!(*inner, AST::VarRef(ref var) if var == "inner") && features.len() == 2 &&
1516            matches!(features[0].0, WrapPosition::Angle{ angle: Value::Float(a1), .. } if
1517            a1 < 91. && a1 > 89.) &&
1518            matches!(features[1].0, WrapPosition::Angle{ offset: Value::Float(o2), .. } if
1519           o2 > 24. && o2 < 26.)
1520        ));
1521
1522        let out = parse_geo("wrap ($inner) with {\n  center => C<2>,\n}");
1523        assert!(matches!(out, Ok(("", AST::Wrap { inner, features })) if
1524            matches!(*inner, AST::VarRef(ref var) if var == "inner") && features.len() == 1 &&
1525            matches!(features[0].0, WrapPosition::Angle{ angle: Value::Float(a1), .. } if
1526            a1 < 0.1 && a1 > -0.1)
1527        ));
1528
1529        let out = parse_geo("wrap ($inner) with {\n  min-left align exterior => C<2>,\n}");
1530        // eprintln!("{:?}", out);
1531        assert!(matches!(out, Ok(("", AST::Wrap { inner, features })) if
1532            matches!(*inner, AST::VarRef(ref var) if var == "inner") && features.len() == 1 &&
1533            matches!(features[0].0, WrapPosition::Corner{ side: Direction::Left, align: crate::Align::End, opposite: false})
1534        ));
1535    }
1536
1537    #[test]
1538    fn test_tuple() {
1539        let out = parse_geo("(C<2>(h))");
1540        eprintln!("{:?}", out);
1541        assert!(
1542            matches!(out, Ok(("", AST::Tuple{ inners })) if inners.len() == 1 &&
1543                matches!(&*inners[0], AST::Circle{ coords: None, radius: r, inner: Some(_) }  if
1544                    r.float() > 1.99 && r.float() < 2.01
1545                )
1546            )
1547        );
1548
1549        let out = parse_geo("(C<2>(h), R<4>)");
1550        assert!(
1551            matches!(out, Ok(("", AST::Tuple{ inners })) if inners.len() == 2 &&
1552                matches!(*inners[1], AST::Rect{ coords: None, size: Some((Value::Float(w), Value::Float(h))), inner: _, rounded: None } if
1553                    w > 3.99 && w < 4.01 && h > 3.99 && h < 4.01
1554                )
1555            )
1556        );
1557
1558        let out = parse_geo("(C<2>(h), (C<2>(h), R<4>))");
1559        // eprintln!("{:?}", out);
1560        assert!(
1561            matches!(out, Ok(("", AST::Tuple{ inners })) if inners.len() == 2 &&
1562                matches!(&*inners[1], AST::Tuple{ inners } if inners.len() == 2 &&
1563                    matches!(*inners[1], AST::Rect{ coords: None, size: Some((Value::Float(w), Value::Float(h))), inner: _, rounded: None } if
1564                        w > 3.99 && w < 4.01 && h > 3.99 && h < 4.01
1565                    )
1566                )
1567            )
1568        );
1569    }
1570
1571    #[test]
1572    fn test_negative() {
1573        let out = parse_geo("negative{C<2>}");
1574        //eprintln!("{:?}", out);
1575        assert!(
1576            matches!(out, Ok(("", AST::Negative{ inners })) if inners.len() == 1 &&
1577                matches!(&*inners[0], AST::Circle{ coords: None, radius: r, inner: None }  if
1578                    r.float() > 1.99 && r.float() < 2.01
1579                )
1580            )
1581        );
1582
1583        let out = parse_geo("negative {\n C<2>,\n   R<4>\n\n}");
1584        // eprintln!("{:?}", out);
1585        assert!(
1586            matches!(out, Ok(("", AST::Negative{ inners })) if inners.len() == 2 &&
1587                matches!(&*inners[0], AST::Circle{ coords: None, radius: r, inner: None }  if
1588                    r.float() > 1.99 && r.float() < 2.01
1589                ) &&
1590                matches!(*inners[1], AST::Rect{ coords: None, size: Some((Value::Float(w), Value::Float(h))), inner: _, rounded: None }  if
1591                    w > 3.99 && w < 4.01 && h > 3.99 && h < 4.01
1592                )
1593            )
1594        );
1595    }
1596
1597    #[test]
1598    fn test_var() {
1599        let out = parse_geo("let bleh = C<25>");
1600        // eprintln!("{:?}", out);
1601        assert!(matches!(out, Ok(("", AST::Assign(var, circ))) if
1602            var == "bleh".to_string() && matches!(*circ, AST::Circle{ .. })));
1603
1604        let out = parse_geo("$bleh");
1605        // eprintln!("{:?}", out);
1606        assert!(matches!(out, Ok(("", AST::VarRef(var))) if var == "bleh".to_string()));
1607
1608        let out = build(
1609            "let rect = column center {
1610          [12] R<7.5>(h)
1611          [11] R<7.5>(h)
1612          [12] R<7.5>(h)
1613        }$rect",
1614        );
1615        // eprintln!("{:?}", out);
1616        assert!(matches!(out, Ok(features) if features.len() == 1));
1617    }
1618
1619    #[test]
1620    fn test_err_msgs() {
1621        let out = build("C<a>");
1622        assert!(matches!(out, Err(Err::Parse(_))));
1623        let out = build("T<a>");
1624        assert!(matches!(out, Err(Err::Parse(_))));
1625
1626        let out = build("R<@(a)>");
1627        // eprintln!("\n\n{}\n\n", match out.err().unwrap() {
1628        //     Err::Parse(e) => e,
1629        //     _ => unreachable!(),
1630        // });
1631        // unreachable!();
1632        assert!(matches!(out, Err(Err::Parse(_))));
1633
1634        let out = build("(aBC)");
1635        assert!(matches!(out, Err(Err::Parse(_))));
1636
1637        let out = build("let bleh = !{aa$%dsfsd + 44}");
1638        assert!(matches!(out, Err(Err::Parse(_))));
1639    }
1640
1641    #[test]
1642    fn test_cel() {
1643        let out = parse_geo("let bleh = !{44}");
1644        assert!(matches!(out, Ok(("", AST::Assign(var, exp))) if
1645            var == "bleh".to_string() && matches!(*exp, AST::Cel(_))));
1646
1647        let out = build("let bleh = !{1 + 1}");
1648        assert!(matches!(out, Ok(_)));
1649        let out = build("let bleh = !{1 + 1}\nlet ye = !{bleh + 2}");
1650        assert!(matches!(out, Ok(_)));
1651
1652        let out = build("let v2 = !{1 * 1}\nR<$v2>");
1653        assert!(matches!(out, Ok(v) if v.len() == 1));
1654
1655        let out = build("R<$missing>");
1656        // eprintln!("{:?}", out);
1657        assert!(matches!(out, Err(Err::UndefinedVariable(_))));
1658
1659        let out = build("let bleh = !{22};\nR<!{bleh + 1}>");
1660        assert!(matches!(out, Ok(v) if v.len() == 1));
1661
1662        let out = build("let bleh = !{5};\nwrap (R<5>) with { left $bleh => R<2>, }");
1663        // eprintln!("{:?}", out);
1664        assert!(matches!(out, Ok(v) if v.len() == 1));
1665    }
1666
1667    #[test]
1668    fn test_comment() {
1669        let out = parse_geo("# yooooooo");
1670        assert!(matches!(out, Ok(("", AST::Comment(msg))) if
1671            msg == " yooooooo"
1672        ));
1673
1674        let out = parse_geo("// yeeeeeeeee");
1675        assert!(matches!(out, Ok(("", AST::Comment(msg))) if
1676            msg == " yeeeeeeeee"
1677        ));
1678    }
1679
1680    #[test]
1681    fn test_rotate() {
1682        let out = parse_geo("rotate(45.0){C<2>}");
1683        eprintln!("{:?}", out);
1684        assert!(
1685            matches!(out, Ok(("", AST::Rotate{ rotation, inners })) if inners.len() == 1 &&
1686                matches!(&*inners[0], AST::Circle{ coords: None, radius: r, inner: None }  if
1687                    r.float() > 1.99 && r.float() < 2.01
1688                ) &&
1689                matches!(rotation.float(), f if f < 45.01 && f > 44.99)
1690            )
1691        );
1692    }
1693}