use crate::model::Shape;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EdgeTok {
pub dashed: bool,
pub no_arrow: bool,
pub label: String,
}
pub struct Scanner {
chars: Vec<char>,
i: usize,
}
impl Scanner {
pub fn new(s: &str) -> Self {
Scanner {
chars: s.chars().collect(),
i: 0,
}
}
pub fn at_end(&self) -> bool {
self.i >= self.chars.len()
}
fn peek(&self) -> Option<char> {
self.chars.get(self.i).copied()
}
fn peek_at(&self, off: usize) -> Option<char> {
self.chars.get(self.i + off).copied()
}
pub fn skip_ws(&mut self) {
while matches!(self.peek(), Some(c) if c == ' ' || c == '\t') {
self.i += 1;
}
}
fn starts_with(&self, pat: &str) -> bool {
pat.chars()
.enumerate()
.all(|(k, c)| self.peek_at(k) == Some(c))
}
pub fn read_id(&mut self) -> Option<String> {
let start = self.i;
while matches!(self.peek(), Some(c) if c.is_ascii_alphanumeric() || c == '_') {
self.i += 1;
}
if self.i == start {
None
} else {
Some(self.chars[start..self.i].iter().collect())
}
}
pub fn read_shape(&mut self) -> Option<(Shape, String)> {
const TWO: &[(&str, &str, Shape)] = &[
("[[", "]]", Shape::Box), ("[(", ")]", Shape::Cylinder), ("([", "])", Shape::Badge), ("((", "))", Shape::Circle), ("{{", "}}", Shape::Hex), ];
for (open, close, shape) in TWO {
if self.starts_with(open) {
self.i += 2;
let label = self.read_until(close);
return Some((*shape, label));
}
}
const ONE: &[(char, &str, Shape)] = &[
('[', "]", Shape::Box), ('(', ")", Shape::Box), ('{', "}", Shape::Diamond), ('>', "]", Shape::Box), ];
for (open, close, shape) in ONE {
if self.peek() == Some(*open) {
self.i += 1;
let label = self.read_until(close);
return Some((*shape, label));
}
}
None
}
fn read_until(&mut self, close: &str) -> String {
let start = self.i;
while !self.at_end() && !self.starts_with(close) {
self.i += 1;
}
let raw: String = self.chars[start..self.i].iter().collect();
if self.starts_with(close) {
self.i += close.chars().count();
}
let t = raw.trim();
if t.len() >= 2 && t.starts_with('"') && t.ends_with('"') {
t[1..t.len() - 1].to_string()
} else {
t.to_string()
}
}
pub fn read_operator(&mut self) -> Option<EdgeTok> {
let save = self.i;
self.skip_ws();
match self.peek() {
Some('-') | Some('=') | Some('<') => {}
_ => {
self.i = save;
return None;
}
}
let run_start = self.i;
while matches!(self.peek(), Some(c) if matches!(c, '-' | '=' | '.' | '>' | '<')) {
self.i += 1;
}
if matches!(self.peek(), Some('x') | Some('o')) && self.i > run_start {
self.i += 1;
}
let run: String = self.chars[run_start..self.i].iter().collect();
let dashed = run.contains('.');
let has_arrow =
run.contains('>') || run.starts_with('<') || run.ends_with('x') || run.ends_with('o');
let mut label = String::new();
self.skip_ws();
if self.peek() == Some('|') {
self.i += 1;
label = self.read_until("|");
}
Some(EdgeTok {
dashed,
no_arrow: !has_arrow,
label,
})
}
}