use std::{
fs::File,
io::{self, Read},
path::Path,
str::from_utf8,
};
use proc_macro2::{LexError, TokenStream};
mod lexer;
use lexer::{lexer, Lexeme::*};
mod parser;
use parser::Parser;
#[cfg(test)]
mod graphviz_tests;
#[macro_export]
macro_rules! rust_dot {
($($graph:tt)+) => {
$crate::_parse_token_stream(quote::quote!($($graph)+), false, false)
}
}
pub fn parse_file<T: AsRef<std::ffi::OsStr>>(file: T) -> io::Result<Parser> {
parse_read(&mut File::open(Path::new(&file))?)
}
pub fn parse_read(read: &mut impl Read) -> io::Result<Parser> {
let mut buf = vec![];
read.read_to_end(&mut buf)?;
Ok(parse_bytes(&buf))
}
pub fn parse_bytes(input: &[u8]) -> Parser {
if let Ok(str) = from_utf8(input) {
return parse_string(str);
}
let (utf16, be) = match &input[0..2] {
&[0xFE, 0xFF] | &[0, _] => (true, true),
&[0xFF, 0xFE] | &[_, 0] => (true, false),
_ => (false, false),
};
if utf16 {
let mut iter = input.iter();
let mut acc = Vec::with_capacity(input.len() / 2_usize);
let join = |a: &u8, b: &u8| (*a as u16) << 8 | (*b as u16);
while let (Some(b0), Some(b1)) = (iter.next(), iter.next()) {
acc.push(if be { join(b0, b1) } else { join(b1, b0) });
}
parse_string(&String::from_utf16(&acc).unwrap())
} else {
parse_string(&input.iter().map(|&c| c as char).collect::<String>())
}
}
pub fn parse_string(input: &str) -> Parser {
let mut input = input;
let owned;
let mut esc = input.contains('\\');
if esc {
esc = false;
let len = input.len();
owned = input
.chars()
.fold(String::with_capacity(len + len / 20), |mut acc, c| {
if esc {
esc = false;
if c == '\\' {
acc += "\\\\"
} else if c != '"' && c != '\n' {
acc.push('\\')
}
} else if c == '\\' {
esc = true;
}
acc.push(c);
acc
});
input = &owned;
esc = true;
}
_parse_token_stream(
input.parse().unwrap_or_else(|e: LexError| {
let span = e.span().start();
panic!(
"parse_*() input not lexically Rust-conforming at {}:{}",
span.line,
span.column + 1
)
}),
true,
esc,
)
}
pub fn _parse_token_stream(input: TokenStream, parse_fn: bool, _esc: bool) -> Parser {
let mut lexer = lexer(input, parse_fn);
let strict = next_if!(lexer, Strict);
let directed = match lexer.next() {
Some(Graph) => false,
Some(DiGraph) => true,
_ => panic!("[ strict ] ( graph | digraph ) expected"),
};
let mut item = lexer.next();
let name = if let Some(Id(name)) = item {
item = lexer.next();
name
} else {
"".into()
};
let Some(Block(block)) = item else {
panic!("After ( graph | digraph ) [ ID ] expected block")
};
if lexer.next().is_some() {
panic!("Nothing expected after main block")
};
Parser::graph(strict, directed, name, block.stream(), parse_fn)
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! validate {
($(($strict:ident $directed:ident $name:literal))? $nodes:literal $edges:literal; $($graph:tt)+) => {{
let graph = rust_dot!($($graph)+);
$(
assert!(graph.strict == $strict);
assert!(graph.directed == $directed);
assert_eq!(graph.name, $name);
)?
assert!(graph.nodes.len() == $nodes);
assert!(graph.edges.len() == $edges);
graph
}}
}
#[test]
fn empty() {
validate! {
(false false "Foo") 0 0;
Graph Foo {}
};
validate! {
(false true "bar") 0 0;
DIGRAPH "bar" {}
};
validate! {
(true false "") 0 0;
strict graph {}
};
}
#[test]
fn simple() {
validate! {
5 3;
digraph {
0 -> 1
2 -> 3 -> 4 [weight = 2, style = fancy];
}
};
}
#[test]
fn cross_product() {
validate! {
7 12;
DIGRAPH "Lacrosse" {
{ 0; 1 } -> { 2 3 ; 4 } -> { 5 [foo = bar] 6 }
}
};
}
#[test]
fn deeply_nested() {
validate! {
8 17;
DiGraph Diggy {
A [oh = boy]
A -> B [x = 1, y = 2 z = 3][zz = 3.1];
A -> { C -> { D:p -> E:q:nw } -> F -> G } -> H;
}
};
}
#[test]
fn attrs() {
validate! {
0 0;
graph 123 {
foo = bar
graph [x= y z = 0];
node [a=1 b=2.1, c=toot d="oho"]
edge [weight = 42]
}
};
}
#[test]
fn bytes() {
let utf16_be = [
0xfe, 0xff, 0, 0x67, 0, 0x72, 0, 0x61, 0, 0x70, 0, 0x68, 0, 0x7b, 0, 0xd8, 0, 0x7d,
];
let utf16_le = [
0xff, 0xfe, 0x67, 0, 0x72, 0, 0x61, 0, 0x70, 0, 0x68, 0, 0x7b, 0, 0xd8, 0, 0x7d, 0,
];
let latin1 = [0x67, 0x72, 0x61, 0x70, 0x68, 0x7b, 0xd8, 0x7d];
for bytes in [
&utf16_be,
&utf16_be[2..],
&utf16_le,
&utf16_le[2..],
"graph{Ø} # don't see comment".as_bytes(),
&latin1,
] {
let graph = parse_bytes(bytes);
assert!(graph.nodes.len() == 1);
}
}
#[test]
fn html() {
validate! {
2 1;
digraph {
< this is <B>bold</B> & first > -> < bella <I>Italica</I> >
}
};
}
}