1use std::collections::HashMap;
8use std::sync::Arc;
9
10use crate::token::*;
11
12#[derive(Clone)]
14pub struct Spanned {
15 pub tok: Token,
16 pub line: u32,
17 pub col: u32,
18 pub arg_frame: Option<Vec<Vec<Spanned>>>,
21 pub macro_frame: Option<Arc<HashMap<String, Vec<Spanned>>>>,
24}
25
26impl Spanned {
27 pub fn new(tok: Token, line: u32, col: u32) -> Self {
28 Self {
29 tok,
30 line,
31 col,
32 arg_frame: None,
33 macro_frame: None,
34 }
35 }
36
37 pub fn with_arg_frame(mut self, args: &[Vec<Spanned>]) -> Self {
38 self.arg_frame = Some(args.to_vec());
39 self
40 }
41
42 pub fn with_macro_frame(mut self, macros: &HashMap<String, Vec<Spanned>>) -> Self {
43 self.macro_frame = Some(Arc::new(macros.clone()));
44 self
45 }
46}
47
48impl std::fmt::Debug for Spanned {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("Spanned")
51 .field("tok", &self.tok)
52 .field("line", &self.line)
53 .field("col", &self.col)
54 .field("arg_frame", &self.arg_frame)
55 .finish_non_exhaustive()
56 }
57}
58
59impl PartialEq for Spanned {
60 fn eq(&self, other: &Self) -> bool {
61 self.tok == other.tok
62 && self.line == other.line
63 && self.col == other.col
64 && self.arg_frame == other.arg_frame
65 }
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct LexError {
71 pub msg: String,
72 pub line: u32,
73 pub col: u32,
74}
75
76impl std::fmt::Display for LexError {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 write!(f, "{}:{}: {}", self.line, self.col, self.msg)
79 }
80}
81
82pub fn lex(src: &str) -> Result<Vec<Spanned>, LexError> {
84 Lexer::new(src).run()
85}
86
87struct Lexer {
88 chars: Vec<char>,
89 pos: usize,
90 line: u32,
91 col: u32,
92 out: Vec<Spanned>,
93}
94
95impl Lexer {
96 fn new(src: &str) -> Self {
97 Lexer {
98 chars: src.chars().collect(),
99 pos: 0,
100 line: 1,
101 col: 1,
102 out: Vec::new(),
103 }
104 }
105
106 fn peek(&self) -> Option<char> {
107 self.chars.get(self.pos).copied()
108 }
109 fn peek_at(&self, n: usize) -> Option<char> {
110 self.chars.get(self.pos + n).copied()
111 }
112
113 fn bump(&mut self) -> Option<char> {
115 let c = self.chars.get(self.pos).copied()?;
116 self.pos += 1;
117 if c == '\n' {
118 self.line += 1;
119 self.col = 1;
120 } else {
121 self.col += 1;
122 }
123 Some(c)
124 }
125
126 fn err<T>(&self, msg: impl Into<String>) -> Result<T, LexError> {
127 Err(LexError {
128 msg: msg.into(),
129 line: self.line,
130 col: self.col,
131 })
132 }
133
134 fn run(mut self) -> Result<Vec<Spanned>, LexError> {
135 loop {
136 loop {
139 match self.peek() {
140 Some(' ') | Some('\t') | Some('\r') => {
141 self.bump();
142 }
143 Some('#') => {
144 while let Some(c) = self.peek() {
145 if c == '\n' {
146 break;
147 }
148 self.bump();
149 }
150 }
151 Some('\\') => {
152 let save = (self.pos, self.line, self.col);
154 self.bump();
155 while matches!(self.peek(), Some(' ') | Some('\t') | Some('\r')) {
156 self.bump();
157 }
158 if self.peek() == Some('\n') {
159 self.bump();
160 } else {
161 self.pos = save.0;
164 self.line = save.1;
165 self.col = save.2;
166 break;
167 }
168 }
169 _ => break,
170 }
171 }
172
173 let (line, col) = (self.line, self.col);
174 let c = match self.peek() {
175 None => {
176 self.push(Token::Eof, line, col);
177 return Ok(self.out);
178 }
179 Some(c) => c,
180 };
181
182 let tok = if c == '\n' || c == ';' {
183 self.bump();
184 Token::Newline
185 } else if c == '"' {
186 self.lex_string()?
187 } else if c == '$' {
188 self.lex_arg()?
189 } else if c.is_ascii_digit()
190 || (c == '.' && self.peek_at(1).is_some_and(|d| d.is_ascii_digit()))
191 {
192 self.lex_number()?
193 } else if c == '.' {
194 self.lex_dot()?
195 } else if c.is_alphabetic() || c == '_' {
196 let tok = self.lex_word();
197 if matches!(tok, Token::Kw(Kw::Sh | Kw::Command)) {
198 self.skip_raw_command_arg();
199 }
200 tok
201 } else {
202 match self.lex_operator()? {
203 Some(t) => t,
204 None => continue, }
206 };
207 self.push(tok, line, col);
208 }
209 }
210
211 fn push(&mut self, tok: Token, line: u32, col: u32) {
212 if tok == Token::Newline
219 && matches!(self.out.last().map(|s| &s.tok), Some(Token::Kw(Kw::Then)))
220 {
221 return;
222 }
223 if tok == Token::Kw(Kw::Then)
224 && matches!(self.out.last().map(|s| &s.tok), Some(Token::Newline))
225 {
226 self.out.pop();
227 }
228 self.out.push(Spanned::new(tok, line, col));
229 }
230
231 fn lex_string(&mut self) -> Result<Token, LexError> {
232 self.bump(); let mut s = String::new();
234 loop {
235 match self.bump() {
236 None => return self.err("unterminated string literal"),
237 Some('"') => break,
238 Some('\\') => {
239 s.push('\\');
242 match self.bump() {
243 Some(c) => s.push(c),
244 None => return self.err("unterminated string literal"),
245 }
246 }
247 Some(c) => s.push(c),
248 }
249 }
250 Ok(Token::Str(s))
251 }
252
253 fn skip_raw_command_arg(&mut self) {
254 while matches!(self.peek(), Some(' ') | Some('\t') | Some('\r')) {
255 self.bump();
256 }
257 if self.peek() == Some('"') {
258 self.skip_raw_quoted();
259 } else if self.starts_word_here("sprintf") {
260 for _ in 0.."sprintf".len() {
261 self.bump();
262 }
263 while matches!(self.peek(), Some(' ') | Some('\t') | Some('\r')) {
264 self.bump();
265 }
266 if self.peek() == Some('(') {
267 self.skip_raw_parens();
268 }
269 } else {
270 self.skip_raw_line_tail();
271 }
272 }
273
274 fn starts_word_here(&self, word: &str) -> bool {
275 for (i, want) in word.chars().enumerate() {
276 if self.peek_at(i) != Some(want) {
277 return false;
278 }
279 }
280 !self
281 .peek_at(word.len())
282 .is_some_and(|c| c.is_alphanumeric() || c == '_')
283 }
284
285 fn skip_raw_quoted(&mut self) {
286 if self.peek() != Some('"') {
287 return;
288 }
289 self.bump();
290 while let Some(c) = self.bump() {
291 match c {
292 '\\' => {
293 self.bump();
294 }
295 '"' if self.raw_quote_is_followed_by(&['\n', ';', '}']) => break,
296 _ => {}
297 }
298 }
299 }
300
301 fn skip_raw_parens(&mut self) {
302 let mut depth = 0i32;
303 while let Some(c) = self.bump() {
304 match c {
305 '"' => self.skip_raw_quoted_tail(),
306 '(' => depth += 1,
307 ')' => {
308 depth -= 1;
309 if depth == 0 {
310 break;
311 }
312 }
313 _ => {}
314 }
315 }
316 }
317
318 fn skip_raw_quoted_tail(&mut self) {
319 while let Some(c) = self.bump() {
320 match c {
321 '\\' => {
322 self.bump();
323 }
324 '"' if self.raw_quote_is_followed_by(&['\n', ';', '}', ',', ')']) => break,
325 _ => {}
326 }
327 }
328 }
329
330 fn raw_quote_is_followed_by(&self, stops: &[char]) -> bool {
331 let mut off = 0;
332 loop {
333 match self.peek_at(off) {
334 Some(' ' | '\t' | '\r') => off += 1,
335 Some(c) => return stops.contains(&c),
336 None => return true,
337 }
338 }
339 }
340
341 fn skip_raw_line_tail(&mut self) {
342 loop {
343 let mut last_non_ws = None;
344 while let Some(c) = self.peek() {
345 if c == '\n' {
346 break;
347 }
348 if !matches!(c, ' ' | '\t' | '\r') {
349 last_non_ws = Some(c);
350 }
351 self.bump();
352 }
353 if self.peek() == Some('\n') && last_non_ws == Some('\\') {
354 self.bump();
355 continue;
356 }
357 break;
358 }
359 }
360
361 fn lex_arg(&mut self) -> Result<Token, LexError> {
362 self.bump(); if self.peek() == Some('+') {
365 self.bump();
366 return Ok(Token::ArgCount);
367 }
368 let mut n = String::new();
369 while let Some(c) = self.peek() {
370 if c.is_ascii_digit() {
371 n.push(c);
372 self.bump();
373 } else {
374 break;
375 }
376 }
377 if n.is_empty() {
378 return Ok(Token::Dollar);
381 }
382 match n.parse() {
383 Ok(v) => Ok(Token::Arg(v)),
384 Err(_) => self.err(format!("macro argument `${n}` is too large")),
385 }
386 }
387
388 fn lex_number(&mut self) -> Result<Token, LexError> {
389 let mut s = String::new();
390 if self.peek() == Some('.') {
391 s.push('0'); }
393 while let Some(c) = self.peek() {
394 if c.is_ascii_digit() {
395 s.push(c);
396 self.bump();
397 } else {
398 break;
399 }
400 }
401 if self.peek() == Some('.') && self.peek_at(1).is_some_and(|d| d.is_ascii_digit()) {
404 s.push('.');
405 self.bump();
406 while let Some(c) = self.peek() {
407 if c.is_ascii_digit() {
408 s.push(c);
409 self.bump();
410 } else {
411 break;
412 }
413 }
414 }
415 if matches!(self.peek(), Some('e') | Some('E'))
417 && (self.peek_at(1).is_some_and(|d| d.is_ascii_digit())
418 || (matches!(self.peek_at(1), Some('+') | Some('-'))
419 && self.peek_at(2).is_some_and(|d| d.is_ascii_digit())))
420 {
421 s.push('e');
422 self.bump();
423 if matches!(self.peek(), Some('+') | Some('-')) {
424 s.push(self.bump().unwrap());
425 }
426 while let Some(c) = self.peek() {
427 if c.is_ascii_digit() {
428 s.push(c);
429 self.bump();
430 } else {
431 break;
432 }
433 }
434 }
435 if matches!(self.peek(), Some('i') | Some('I'))
439 && !self
440 .peek_at(1)
441 .is_some_and(|c| c.is_alphanumeric() || c == '_')
442 {
443 self.bump();
444 }
445 match s.parse::<f64>() {
446 Ok(v) => Ok(Token::Float(v)),
447 Err(_) => self.err(format!("invalid number `{s}`")),
448 }
449 }
450
451 fn lex_dot(&mut self) -> Result<Token, LexError> {
454 let save = (self.pos, self.line, self.col);
455 self.bump(); let mut w = String::new();
458 while let Some(c) = self.peek() {
459 if c.is_alphanumeric() || c == '_' {
460 w.push(c);
461 self.bump();
462 } else {
463 break;
464 }
465 }
466 if let Some(tok) = dot_keyword(&w) {
467 Ok(tok)
468 } else {
469 self.pos = save.0 + 1;
472 self.line = save.1;
473 self.col = save.2 + 1;
474 Ok(Token::Dot)
475 }
476 }
477
478 fn lex_word(&mut self) -> Token {
479 let mut w = String::new();
480 while let Some(c) = self.peek() {
481 if c.is_alphanumeric() || c == '_' {
482 w.push(c);
483 self.bump();
484 } else {
485 break;
486 }
487 }
488 word_keyword(&w)
489 }
490
491 fn lex_operator(&mut self) -> Result<Option<Token>, LexError> {
492 let c = self.bump().unwrap();
493 let next = self.peek();
494 let tok = match c {
495 '(' => Token::Lparen,
496 ')' => Token::Rparen,
497 ',' => Token::Comma,
498 '^' => Token::Caret,
499 '{' => Token::LeftBrace,
500 '}' => Token::RightBrace,
501 ']' => Token::RightBrack,
502 '`' => Token::LeftQuote,
503 '\'' => Token::RightQuote,
504 '[' => {
505 if next == Some(']') {
506 self.bump();
507 Token::Block
508 } else {
509 Token::LeftBrack
510 }
511 }
512 ':' => self.two('=', Token::ColonEq, Token::Colon),
513 '=' => self.two('=', Token::EqEq, Token::Eq),
514 '!' => self.two('=', Token::Neq, Token::Not),
515 '+' => self.two('=', Token::PlusEq, Token::Plus),
516 '*' => self.two('=', Token::MultEq, Token::Mult),
517 '/' => self.two('=', Token::DivEq, Token::Div),
518 '%' => self.two('=', Token::RemEq, Token::Percent),
519 '>' => self.two('=', Token::Ge, Token::Gt),
520 '&' => self.two('&', Token::AndAnd, Token::Ampersand),
521 '|' => {
522 if next == Some('|') {
523 self.bump();
524 Token::OrOr
525 } else {
526 return self.err("unexpected `|`");
527 }
528 }
529 '-' => match next {
530 Some('>') => {
531 self.bump();
532 Token::Arrow(Arrow::Right)
533 }
534 Some('=') => {
535 self.bump();
536 Token::MinusEq
537 }
538 _ => Token::Minus,
539 },
540 '<' => match next {
541 Some('=') => {
542 self.bump();
543 Token::Le
544 }
545 Some('-') => {
546 self.bump();
547 if self.peek() == Some('>') {
548 self.bump();
549 Token::Arrow(Arrow::Double)
550 } else {
551 Token::Arrow(Arrow::Left)
552 }
553 }
554 _ => Token::Lt,
555 },
556 '\\' => Token::Backslash,
561 other => return self.err(format!("unexpected character `{other}`")),
562 };
563 Ok(Some(tok))
564 }
565
566 fn two(&mut self, c: char, yes: Token, no: Token) -> Token {
568 if self.peek() == Some(c) {
569 self.bump();
570 yes
571 } else {
572 no
573 }
574 }
575}
576
577fn word_keyword(w: &str) -> Token {
580 use Token::*;
581 match w {
582 "box" => Prim(self::Prim::Box),
584 "circle" => Prim(self::Prim::Circle),
585 "ellipse" => Prim(self::Prim::Ellipse),
586 "arc" => Prim(self::Prim::Arc),
587 "line" => Prim(self::Prim::Line),
588 "arrow" => Prim(self::Prim::Arrow),
589 "move" => Prim(self::Prim::Move),
590 "spline" => Prim(self::Prim::Spline),
591 "up" => Dir(self::Dir::Up),
593 "down" => Dir(self::Dir::Down),
594 "right" => Dir(self::Dir::Right),
595 "left" => Dir(self::Dir::Left),
596 "height" | "ht" => Kw(self::Kw::Ht),
598 "width" | "wid" => Kw(self::Kw::Wid),
599 "radius" | "rad" => Kw(self::Kw::Rad),
600 "diameter" | "diam" => Kw(self::Kw::Diam),
601 "thickness" | "thick" => Kw(self::Kw::Thick),
602 "scaled" => Kw(self::Kw::Scaled),
603 "from" => Kw(self::Kw::From),
604 "to" => Kw(self::Kw::To),
605 "at" => Kw(self::Kw::At),
606 "with" => Kw(self::Kw::With),
607 "by" => Kw(self::Kw::By),
608 "then" => Kw(self::Kw::Then),
609 "cw" => Kw(self::Kw::Cw),
610 "ccw" => Kw(self::Kw::Ccw),
611 "continue" => Kw(self::Kw::Continue),
612 "chop" => Kw(self::Kw::Chop),
613 "same" => Kw(self::Kw::Same),
614 "of" => Kw(self::Kw::Of),
615 "the" => Kw(self::Kw::The),
616 "way" => Kw(self::Kw::Way),
617 "between" => Kw(self::Kw::Between),
618 "and" => Kw(self::Kw::And),
619 "last" => Kw(self::Kw::Last),
620 "fill" | "filled" => Kw(self::Kw::Fill),
621 "st" | "nd" | "rd" | "th" => Kw(self::Kw::Nth),
622 "Here" => Kw(self::Kw::Here),
623 "top" => Corner(self::Corner::N),
625 "bottom" => Corner(self::Corner::S),
626 "start" => Corner(self::Corner::Start),
627 "end" => Corner(self::Corner::End),
628 "print" => Kw(self::Kw::Print),
630 "copy" => Kw(self::Kw::Copy),
631 "reset" => Kw(self::Kw::Reset),
632 "exec" => Kw(self::Kw::Exec),
633 "sh" => Kw(self::Kw::Sh),
634 "command" => Kw(self::Kw::Command),
635 "define" => Kw(self::Kw::Define),
636 "undefine" | "undef" => Kw(self::Kw::Undef),
637 "rand" => Kw(self::Kw::Rand),
638 "if" => Kw(self::Kw::If),
639 "else" => Kw(self::Kw::Else),
640 "for" => Kw(self::Kw::For),
641 "do" => Kw(self::Kw::Do),
642 "sprintf" => Kw(self::Kw::Sprintf),
643 "animate" => Kw(self::Kw::Animate),
645 "after" => Kw(self::Kw::After),
646 "delay" => Kw(self::Kw::Delay),
647 "solid" => LineType(self::LineType::Solid),
649 "dotted" => LineType(self::LineType::Dotted),
650 "dashed" => LineType(self::LineType::Dashed),
651 "invis" | "invisible" => LineType(self::LineType::Invis),
652 "color" | "colour" | "colored" | "coloured" => Color(self::Color::Colored),
654 "outline" | "outlined" => Color(self::Color::Outlined),
655 "shade" | "shaded" => Color(self::Color::Shaded),
656 "center" | "centre" => TextPos(self::TextPos::Center),
658 "ljust" => TextPos(self::TextPos::Ljust),
659 "rjust" => TextPos(self::TextPos::Rjust),
660 "above" => TextPos(self::TextPos::Above),
661 "below" => TextPos(self::TextPos::Below),
662 "abs" => Func1(self::Func1::Abs),
664 "acos" => Func1(self::Func1::Acos),
665 "asin" => Func1(self::Func1::Asin),
666 "cos" => Func1(self::Func1::Cos),
667 "exp" => Func1(self::Func1::Exp),
668 "expe" => Func1(self::Func1::Expe),
669 "int" => Func1(self::Func1::Int),
670 "log" => Func1(self::Func1::Log),
671 "loge" => Func1(self::Func1::Loge),
672 "sign" => Func1(self::Func1::Sign),
673 "sin" => Func1(self::Func1::Sin),
674 "sqrt" => Func1(self::Func1::Sqrt),
675 "tan" => Func1(self::Func1::Tan),
676 "floor" => Func1(self::Func1::Floor),
677 "atan2" => Func2(self::Func2::Atan2),
679 "max" => Func2(self::Func2::Max),
680 "min" => Func2(self::Func2::Min),
681 "pmod" => Func2(self::Func2::Pmod),
682 "arcrad" => EnvVar(self::EnvVar::Arcrad),
684 "arrowht" => EnvVar(self::EnvVar::Arrowht),
685 "arrowwid" => EnvVar(self::EnvVar::Arrowwid),
686 "boxht" => EnvVar(self::EnvVar::Boxht),
687 "boxrad" => EnvVar(self::EnvVar::Boxrad),
688 "boxwid" => EnvVar(self::EnvVar::Boxwid),
689 "circlerad" => EnvVar(self::EnvVar::Circlerad),
690 "dashwid" => EnvVar(self::EnvVar::Dashwid),
691 "ellipseht" => EnvVar(self::EnvVar::Ellipseht),
692 "ellipsewid" => EnvVar(self::EnvVar::Ellipsewid),
693 "lineht" => EnvVar(self::EnvVar::Lineht),
694 "linewid" => EnvVar(self::EnvVar::Linewid),
695 "moveht" => EnvVar(self::EnvVar::Moveht),
696 "movewid" => EnvVar(self::EnvVar::Movewid),
697 "textht" => EnvVar(self::EnvVar::Textht),
698 "textoffset" => EnvVar(self::EnvVar::Textoffset),
699 "textwid" => EnvVar(self::EnvVar::Textwid),
700 "arrowhead" => EnvVar(self::EnvVar::Arrowhead),
701 "fillval" => EnvVar(self::EnvVar::Fillval),
702 "linethick" => EnvVar(self::EnvVar::Linethick),
703 "margin" => EnvVar(self::EnvVar::Margin),
704 "topmargin" => EnvVar(self::EnvVar::Topmargin),
705 "rightmargin" => EnvVar(self::EnvVar::Rightmargin),
706 "bottommargin" => EnvVar(self::EnvVar::Bottommargin),
707 "leftmargin" => EnvVar(self::EnvVar::Leftmargin),
708 "maxpsht" => EnvVar(self::EnvVar::Maxpsht),
709 "maxpswid" => EnvVar(self::EnvVar::Maxpswid),
710 "scale" => EnvVar(self::EnvVar::Scale),
711 "texlabels" => EnvVar(self::EnvVar::Texlabels),
712 _ => {
714 if w.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
715 Label(w.to_string())
716 } else {
717 Name(w.to_string())
718 }
719 }
720 }
721}
722
723fn dot_keyword(w: &str) -> Option<Token> {
726 use Token::*;
727 let t = match w {
728 "PS" => DotPS,
729 "PE" => DotPE,
730 "x" => DotX,
731 "y" => DotY,
732 "ne" => Corner(self::Corner::Ne),
734 "se" => Corner(self::Corner::Se),
735 "nw" => Corner(self::Corner::Nw),
736 "sw" => Corner(self::Corner::Sw),
737 "n" | "t" | "top" | "north" => Corner(self::Corner::N),
738 "s" | "b" | "bot" | "bottom" | "south" => Corner(self::Corner::S),
739 "e" | "r" | "east" | "right" => Corner(self::Corner::E),
740 "w" | "l" | "west" | "left" => Corner(self::Corner::W),
741 "start" => Corner(self::Corner::Start),
742 "end" => Corner(self::Corner::End),
743 "c" | "center" | "centre" => Corner(self::Corner::Center),
744 "ht" | "height" => Param(self::Param::Height),
746 "wid" | "width" => Param(self::Param::Width),
747 "rad" | "radius" => Param(self::Param::Radius),
748 "diam" | "diameter" => Param(self::Param::Diameter),
749 "thick" | "thickness" => Param(self::Param::Thickness),
750 "len" | "length" => Param(self::Param::Length),
751 _ => return None,
752 };
753 Some(t)
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759
760 fn toks(src: &str) -> Vec<Token> {
761 lex(src).unwrap().into_iter().map(|s| s.tok).collect()
762 }
763
764 #[test]
765 fn basic_box() {
766 assert_eq!(
767 toks("box \"hi\""),
768 vec![Token::Prim(Prim::Box), Token::Str("hi".into()), Token::Eof]
769 );
770 }
771
772 #[test]
773 fn label_vs_name() {
774 assert_eq!(
775 toks("Start: boxwid"),
776 vec![
777 Token::Label("Start".into()),
778 Token::Colon,
779 Token::EnvVar(EnvVar::Boxwid),
780 Token::Eof
781 ]
782 );
783 assert_eq!(toks("myvar"), vec![Token::Name("myvar".into()), Token::Eof]);
785 }
786
787 #[test]
788 fn numbers() {
789 assert_eq!(toks("0.5"), vec![Token::Float(0.5), Token::Eof]);
790 assert_eq!(toks(".25"), vec![Token::Float(0.25), Token::Eof]);
791 assert_eq!(toks("1e3"), vec![Token::Float(1000.0), Token::Eof]);
792 assert_eq!(toks("2.5e-1"), vec![Token::Float(0.25), Token::Eof]);
793 }
794
795 #[test]
796 fn operators_and_arrows() {
797 assert_eq!(
798 toks("a := b <= c -> d <- e <-> f"),
799 vec![
800 Token::Name("a".into()),
801 Token::ColonEq,
802 Token::Name("b".into()),
803 Token::Le,
804 Token::Name("c".into()),
805 Token::Arrow(Arrow::Right),
806 Token::Name("d".into()),
807 Token::Arrow(Arrow::Left),
808 Token::Name("e".into()),
809 Token::Arrow(Arrow::Double),
810 Token::Name("f".into()),
811 Token::Eof
812 ]
813 );
814 }
815
816 #[test]
817 fn minus_is_not_arrow_without_gt() {
818 assert_eq!(
819 toks("2-3"),
820 vec![
821 Token::Float(2.0),
822 Token::Minus,
823 Token::Float(3.0),
824 Token::Eof
825 ]
826 );
827 }
828
829 #[test]
830 fn compass_and_params() {
831 assert_eq!(
832 toks("last box.ne A.ht .center"),
833 vec![
834 Token::Kw(Kw::Last),
835 Token::Prim(Prim::Box),
836 Token::Corner(Corner::Ne),
837 Token::Label("A".into()),
838 Token::Param(Param::Height),
839 Token::Corner(Corner::Center),
840 Token::Eof
841 ]
842 );
843 }
844
845 #[test]
846 fn dot_then_label() {
847 assert_eq!(
849 toks("B.A"),
850 vec![
851 Token::Label("B".into()),
852 Token::Dot,
853 Token::Label("A".into()),
854 Token::Eof
855 ]
856 );
857 }
858
859 #[test]
860 fn statement_separators_and_comments() {
861 assert_eq!(
862 toks("box; circle # a comment\narc"),
863 vec![
864 Token::Prim(Prim::Box),
865 Token::Newline,
866 Token::Prim(Prim::Circle),
867 Token::Newline,
868 Token::Prim(Prim::Arc),
869 Token::Eof
870 ]
871 );
872 }
873
874 #[test]
875 fn shell_commands_skip_raw_line_tail() {
876 assert_eq!(
877 toks("sh \"echo -n \\\"print \\\\\\\"\\\" > $1_prow\"\nbox"),
878 vec![
879 Token::Kw(Kw::Sh),
880 Token::Newline,
881 Token::Prim(Prim::Box),
882 Token::Eof
883 ]
884 );
885 assert_eq!(
886 toks("command \\foo $bad \"unterminated\ncircle"),
887 vec![
888 Token::Kw(Kw::Command),
889 Token::Newline,
890 Token::Prim(Prim::Circle),
891 Token::Eof
892 ]
893 );
894 assert_eq!(
895 toks("sh \"sed something \\\n > tmp\"\narc"),
896 vec![
897 Token::Kw(Kw::Sh),
898 Token::Newline,
899 Token::Prim(Prim::Arc),
900 Token::Eof
901 ]
902 );
903 assert_eq!(
904 toks("sh \"rm\";}\ncommand sprintf(\"x\", y) }\n"),
905 vec![
906 Token::Kw(Kw::Sh),
907 Token::Newline,
908 Token::RightBrace,
909 Token::Newline,
910 Token::Kw(Kw::Command),
911 Token::RightBrace,
912 Token::Newline,
913 Token::Eof
914 ]
915 );
916 assert_eq!(
917 toks("sh \"echo -n \\\"print \\\\\"\\\" > $1_prow\"\nbox"),
918 vec![
919 Token::Kw(Kw::Sh),
920 Token::Newline,
921 Token::Prim(Prim::Box),
922 Token::Eof
923 ]
924 );
925 }
926
927 #[test]
928 fn line_continuation() {
929 assert_eq!(
930 toks("box \\\n wid 2"),
931 vec![
932 Token::Prim(Prim::Box),
933 Token::Kw(Kw::Wid),
934 Token::Float(2.0),
935 Token::Eof
936 ]
937 );
938 }
939
940 #[test]
941 fn ps_pe_and_block() {
942 assert_eq!(
943 toks(".PS\nbox\n.PE"),
944 vec![
945 Token::DotPS,
946 Token::Newline,
947 Token::Prim(Prim::Box),
948 Token::Newline,
949 Token::DotPE,
950 Token::Eof
951 ]
952 );
953 assert_eq!(toks("[]"), vec![Token::Block, Token::Eof]);
954 assert_eq!(
955 toks("[ box ]"),
956 vec![
957 Token::LeftBrack,
958 Token::Prim(Prim::Box),
959 Token::RightBrack,
960 Token::Eof
961 ]
962 );
963 }
964
965 #[test]
966 fn macro_arg() {
967 assert_eq!(
968 toks("box $1"),
969 vec![Token::Prim(Prim::Box), Token::Arg(1), Token::Eof]
970 );
971 }
972
973 #[test]
974 fn func_and_envvar() {
975 assert_eq!(
976 toks("sqrt(2) atan2 scale margin topmargin"),
977 vec![
978 Token::Func1(Func1::Sqrt),
979 Token::Lparen,
980 Token::Float(2.0),
981 Token::Rparen,
982 Token::Func2(Func2::Atan2),
983 Token::EnvVar(EnvVar::Scale),
984 Token::EnvVar(EnvVar::Margin),
985 Token::EnvVar(EnvVar::Topmargin),
986 Token::Eof
987 ]
988 );
989 }
990
991 #[test]
992 fn newline_after_then_is_continuation() {
993 assert_eq!(
995 toks("line right then\nup"),
996 vec![
997 Token::Prim(Prim::Line),
998 Token::Dir(Dir::Right),
999 Token::Kw(Kw::Then),
1000 Token::Dir(Dir::Up),
1001 Token::Eof,
1002 ]
1003 );
1004 assert_eq!(
1005 toks("box\nup"),
1006 vec![
1007 Token::Prim(Prim::Box),
1008 Token::Newline,
1009 Token::Dir(Dir::Up),
1010 Token::Eof,
1011 ]
1012 );
1013 }
1014
1015 #[test]
1016 fn newline_before_then_is_continuation() {
1017 assert_eq!(
1019 toks("line right\nthen up"),
1020 vec![
1021 Token::Prim(Prim::Line),
1022 Token::Dir(Dir::Right),
1023 Token::Kw(Kw::Then),
1024 Token::Dir(Dir::Up),
1025 Token::Eof,
1026 ]
1027 );
1028 }
1029
1030 #[test]
1031 fn dollar_and_backslash_are_literal_text() {
1032 assert_eq!(
1035 toks("$\\beta$"),
1036 vec![
1037 Token::Dollar,
1038 Token::Backslash,
1039 Token::Name("beta".into()),
1040 Token::Dollar,
1041 Token::Eof,
1042 ]
1043 );
1044 assert_eq!(toks("$1"), vec![Token::Arg(1), Token::Eof]);
1046 }
1047}