2024-07-24 11:54:04 +02:00
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::ops::Range;
|
|
|
|
use std::rc::Rc;
|
|
|
|
use std::sync::Once;
|
|
|
|
|
|
|
|
use crate::parser::util::Property;
|
|
|
|
use crate::parser::util::PropertyMapError;
|
|
|
|
use crate::parser::util::PropertyParser;
|
|
|
|
use ariadne::Fmt;
|
|
|
|
use ariadne::Label;
|
|
|
|
use ariadne::Report;
|
|
|
|
use ariadne::ReportKind;
|
|
|
|
use crypto::digest::Digest;
|
|
|
|
use crypto::sha2::Sha512;
|
|
|
|
use graphviz_rust::cmd::Format;
|
|
|
|
use graphviz_rust::cmd::Layout;
|
|
|
|
use graphviz_rust::exec_dot;
|
|
|
|
use mlua::Function;
|
|
|
|
use mlua::Lua;
|
|
|
|
use regex::Captures;
|
|
|
|
use regex::Regex;
|
|
|
|
|
|
|
|
use crate::cache::cache::Cached;
|
|
|
|
use crate::cache::cache::CachedError;
|
|
|
|
use crate::compiler::compiler::Compiler;
|
|
|
|
use crate::compiler::compiler::Target;
|
|
|
|
use crate::document::document::Document;
|
|
|
|
use crate::document::element::ElemKind;
|
|
|
|
use crate::document::element::Element;
|
|
|
|
use crate::parser::parser::Parser;
|
|
|
|
use crate::parser::rule::RegexRule;
|
|
|
|
use crate::parser::source::Source;
|
|
|
|
use crate::parser::source::Token;
|
|
|
|
use crate::parser::util;
|
2024-07-24 09:09:28 +02:00
|
|
|
|
|
|
|
#[derive(Debug)]
|
2024-07-24 11:54:04 +02:00
|
|
|
struct Graphviz {
|
|
|
|
pub location: Token,
|
|
|
|
pub dot: String,
|
|
|
|
pub layout: Layout,
|
2024-07-24 13:34:00 +02:00
|
|
|
pub width: String,
|
2024-07-24 11:54:04 +02:00
|
|
|
pub caption: Option<String>,
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn layout_from_str(value: &str) -> Result<Layout, String> {
|
|
|
|
match value {
|
|
|
|
"dot" => Ok(Layout::Dot),
|
|
|
|
"neato" => Ok(Layout::Neato),
|
|
|
|
"fdp" => Ok(Layout::Fdp),
|
|
|
|
"sfdp" => Ok(Layout::Sfdp),
|
|
|
|
"circo" => Ok(Layout::Circo),
|
|
|
|
"twopi" => Ok(Layout::Twopi),
|
|
|
|
"osage" => Ok(Layout::Asage), // typo in graphviz_rust ?
|
|
|
|
"patchwork" => Ok(Layout::Patchwork),
|
|
|
|
_ => Err(format!("Unknown layout: {value}")),
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
impl Graphviz {
|
|
|
|
/// Renders dot to svg
|
|
|
|
fn dot_to_svg(&self) -> Result<String, String> {
|
|
|
|
print!("Rendering Graphviz `{}`... ", self.dot);
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
let svg = match exec_dot(
|
|
|
|
self.dot.clone(),
|
|
|
|
vec![self.layout.into(), Format::Svg.into()],
|
|
|
|
) {
|
|
|
|
Ok(svg) => {
|
|
|
|
let out = String::from_utf8_lossy(svg.as_slice());
|
2024-07-24 13:34:00 +02:00
|
|
|
let svg_start = out.find("<svg").unwrap(); // Remove svg header
|
|
|
|
let split_at = out.split_at(svg_start).1.find('\n').unwrap();
|
2024-07-24 11:54:04 +02:00
|
|
|
|
2024-07-24 13:34:00 +02:00
|
|
|
let mut result = format!("<svg width=\"{}\"", self.width);
|
|
|
|
result.push_str(out.split_at(svg_start+split_at).1);
|
|
|
|
|
|
|
|
result
|
2024-07-24 11:54:04 +02:00
|
|
|
}
|
|
|
|
Err(e) => return Err(format!("Unable to execute dot: {e}")),
|
|
|
|
};
|
2024-07-24 09:09:28 +02:00
|
|
|
println!("Done!");
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
Ok(svg)
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
impl Cached for Graphviz {
|
|
|
|
type Key = String;
|
|
|
|
type Value = String;
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn sql_table() -> &'static str {
|
|
|
|
"CREATE TABLE IF NOT EXISTS cached_dot (
|
2024-07-24 09:09:28 +02:00
|
|
|
digest TEXT PRIMARY KEY,
|
|
|
|
svg BLOB NOT NULL);"
|
2024-07-24 11:54:04 +02:00
|
|
|
}
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn sql_get_query() -> &'static str { "SELECT svg FROM cached_dot WHERE digest = (?1)" }
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn sql_insert_query() -> &'static str { "INSERT INTO cached_dot (digest, svg) VALUES (?1, ?2)" }
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn key(&self) -> <Self as Cached>::Key {
|
2024-07-24 09:09:28 +02:00
|
|
|
let mut hasher = Sha512::new();
|
2024-07-24 11:54:04 +02:00
|
|
|
hasher.input((self.layout as usize).to_be_bytes().as_slice());
|
|
|
|
hasher.input(self.dot.as_bytes());
|
2024-07-24 09:09:28 +02:00
|
|
|
|
|
|
|
hasher.result_str()
|
2024-07-24 11:54:04 +02:00
|
|
|
}
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
impl Element for Graphviz {
|
|
|
|
fn location(&self) -> &Token { &self.location }
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn kind(&self) -> ElemKind { ElemKind::Block }
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn element_name(&self) -> &'static str { "Graphviz" }
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn to_string(&self) -> String { format!("{self:#?}") }
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> {
|
2024-07-24 09:09:28 +02:00
|
|
|
match compiler.target() {
|
|
|
|
Target::HTML => {
|
2024-07-24 11:54:04 +02:00
|
|
|
static CACHE_INIT: Once = Once::new();
|
|
|
|
CACHE_INIT.call_once(|| {
|
|
|
|
if let Some(mut con) = compiler.cache() {
|
|
|
|
if let Err(e) = Graphviz::init(&mut con) {
|
|
|
|
eprintln!("Unable to create cache table: {e}");
|
|
|
|
}
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
|
|
|
});
|
2024-07-24 13:20:29 +02:00
|
|
|
// TODO: Format svg in a div
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
if let Some(mut con) = compiler.cache() {
|
|
|
|
match self.cached(&mut con, |s| s.dot_to_svg()) {
|
2024-07-24 09:09:28 +02:00
|
|
|
Ok(s) => Ok(s),
|
2024-07-24 11:54:04 +02:00
|
|
|
Err(e) => match e {
|
|
|
|
CachedError::SqlErr(e) => {
|
|
|
|
Err(format!("Querying the cache failed: {e}"))
|
|
|
|
}
|
|
|
|
CachedError::GenErr(e) => Err(e),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
match self.dot_to_svg() {
|
|
|
|
Ok(svg) => Ok(svg),
|
|
|
|
Err(e) => Err(e),
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-24 11:54:04 +02:00
|
|
|
_ => todo!("Unimplemented"),
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
2024-07-24 11:54:04 +02:00
|
|
|
}
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
pub struct GraphRule {
|
|
|
|
re: [Regex; 1],
|
|
|
|
properties: PropertyParser,
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
impl GraphRule {
|
2024-07-24 09:09:28 +02:00
|
|
|
pub fn new() -> Self {
|
2024-07-24 11:54:04 +02:00
|
|
|
let mut props = HashMap::new();
|
|
|
|
props.insert(
|
|
|
|
"layout".to_string(),
|
|
|
|
Property::new(
|
|
|
|
true,
|
|
|
|
"Graphviz layout engine see <https://graphviz.org/docs/layouts/>".to_string(),
|
|
|
|
Some("dot".to_string()),
|
|
|
|
),
|
|
|
|
);
|
2024-07-24 13:34:00 +02:00
|
|
|
props.insert(
|
|
|
|
"width".to_string(),
|
|
|
|
Property::new(
|
|
|
|
true,
|
|
|
|
"SVG width".to_string(),
|
|
|
|
Some("100%".to_string()),
|
|
|
|
),
|
|
|
|
);
|
2024-07-24 09:09:28 +02:00
|
|
|
Self {
|
2024-07-24 11:54:04 +02:00
|
|
|
re: [Regex::new(
|
|
|
|
r"\[graph\](?:\[((?:\\.|[^\[\]\\])*?)\])?(?:((?:\\.|[^\\\\])*?)\[/graph\])?",
|
|
|
|
)
|
|
|
|
.unwrap()],
|
|
|
|
properties: PropertyParser::new(props),
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
impl RegexRule for GraphRule {
|
|
|
|
fn name(&self) -> &'static str { "Graph" }
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn regexes(&self) -> &[regex::Regex] { &self.re }
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
fn on_regex_match(
|
|
|
|
&self,
|
|
|
|
_: usize,
|
|
|
|
parser: &dyn Parser,
|
|
|
|
document: &dyn Document,
|
|
|
|
token: Token,
|
|
|
|
matches: Captures,
|
|
|
|
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
|
2024-07-24 09:09:28 +02:00
|
|
|
let mut reports = vec![];
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
let graph_content = match matches.get(2) {
|
|
|
|
// Unterminated `[graph]`
|
2024-07-24 09:09:28 +02:00
|
|
|
None => {
|
|
|
|
reports.push(
|
|
|
|
Report::build(ReportKind::Error, token.source(), token.start())
|
2024-07-24 11:54:04 +02:00
|
|
|
.with_message("Unterminated Graph Code")
|
|
|
|
.with_label(
|
|
|
|
Label::new((token.source().clone(), token.range.clone()))
|
|
|
|
.with_message(format!(
|
|
|
|
"Missing terminating `{}` after first `{}`",
|
|
|
|
"[/graph]".fg(parser.colors().info),
|
|
|
|
"[graph]".fg(parser.colors().info)
|
|
|
|
))
|
|
|
|
.with_color(parser.colors().error),
|
|
|
|
)
|
|
|
|
.finish(),
|
|
|
|
);
|
2024-07-24 09:09:28 +02:00
|
|
|
return reports;
|
|
|
|
}
|
|
|
|
Some(content) => {
|
2024-07-24 11:54:04 +02:00
|
|
|
let processed = util::process_escaped(
|
|
|
|
'\\',
|
|
|
|
"[/graph]",
|
|
|
|
content.as_str().trim_start().trim_end(),
|
|
|
|
);
|
2024-07-24 09:09:28 +02:00
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
if processed.is_empty() {
|
2024-07-24 09:09:28 +02:00
|
|
|
reports.push(
|
2024-07-24 11:54:04 +02:00
|
|
|
Report::build(ReportKind::Error, token.source(), content.start())
|
|
|
|
.with_message("Empty Graph Code")
|
|
|
|
.with_label(
|
|
|
|
Label::new((token.source().clone(), content.range()))
|
|
|
|
.with_message("Graph code is empty")
|
|
|
|
.with_color(parser.colors().error),
|
|
|
|
)
|
|
|
|
.finish(),
|
|
|
|
);
|
|
|
|
return reports;
|
2024-07-24 09:09:28 +02:00
|
|
|
}
|
|
|
|
processed
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
// Properties
|
|
|
|
let properties = match matches.get(1) {
|
|
|
|
None => match self.properties.default() {
|
|
|
|
Ok(properties) => properties,
|
|
|
|
Err(e) => {
|
|
|
|
reports.push(
|
|
|
|
Report::build(ReportKind::Error, token.source(), token.start())
|
|
|
|
.with_message("Invalid Graph")
|
|
|
|
.with_label(
|
|
|
|
Label::new((token.source().clone(), token.range.clone()))
|
|
|
|
.with_message(format!("Graph is missing property: {e}"))
|
|
|
|
.with_color(parser.colors().error),
|
|
|
|
)
|
|
|
|
.finish(),
|
|
|
|
);
|
|
|
|
return reports;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
Some(props) => {
|
|
|
|
let processed =
|
|
|
|
util::process_escaped('\\', "]", props.as_str().trim_start().trim_end());
|
|
|
|
match self.properties.parse(processed.as_str()) {
|
|
|
|
Err(e) => {
|
|
|
|
reports.push(
|
|
|
|
Report::build(ReportKind::Error, token.source(), props.start())
|
|
|
|
.with_message("Invalid Graph Properties")
|
|
|
|
.with_label(
|
|
|
|
Label::new((token.source().clone(), props.range()))
|
|
|
|
.with_message(e)
|
|
|
|
.with_color(parser.colors().error),
|
|
|
|
)
|
|
|
|
.finish(),
|
|
|
|
);
|
|
|
|
return reports;
|
|
|
|
}
|
|
|
|
Ok(properties) => properties,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Property "layout"
|
|
|
|
let graph_layout = match properties.get("layout", |prop, value| {
|
|
|
|
layout_from_str(value.as_str()).map_err(|e| (prop, e))
|
|
|
|
}) {
|
|
|
|
Ok((_prop, kind)) => kind,
|
|
|
|
Err(e) => match e {
|
|
|
|
PropertyMapError::ParseError((prop, err)) => {
|
|
|
|
reports.push(
|
|
|
|
Report::build(ReportKind::Error, token.source(), token.start())
|
|
|
|
.with_message("Invalid Graph Property")
|
|
|
|
.with_label(
|
|
|
|
Label::new((token.source().clone(), token.range.clone()))
|
|
|
|
.with_message(format!(
|
|
|
|
"Property `layout: {}` cannot be converted: {}",
|
|
|
|
prop.fg(parser.colors().info),
|
|
|
|
err.fg(parser.colors().error)
|
|
|
|
))
|
|
|
|
.with_color(parser.colors().warning),
|
|
|
|
)
|
|
|
|
.finish(),
|
|
|
|
);
|
|
|
|
return reports;
|
|
|
|
}
|
|
|
|
PropertyMapError::NotFoundError(err) => {
|
|
|
|
reports.push(
|
|
|
|
Report::build(ReportKind::Error, token.source(), token.start())
|
|
|
|
.with_message("Invalid Graph Property")
|
|
|
|
.with_label(
|
|
|
|
Label::new((
|
|
|
|
token.source().clone(),
|
|
|
|
token.start() + 1..token.end(),
|
|
|
|
))
|
2024-07-25 13:13:12 +02:00
|
|
|
.with_message(err)
|
2024-07-24 11:54:04 +02:00
|
|
|
.with_color(parser.colors().warning),
|
|
|
|
)
|
|
|
|
.finish(),
|
|
|
|
);
|
|
|
|
return reports;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2024-07-24 13:34:00 +02:00
|
|
|
// FIXME: You can escape html, make sure we escape single "
|
|
|
|
// Property "width"
|
|
|
|
let graph_width = match properties.get("width", |_, value| -> Result<String, ()> {
|
|
|
|
Ok(value.clone())
|
|
|
|
}) {
|
|
|
|
Ok((_, kind)) => kind,
|
|
|
|
Err(e) => match e {
|
|
|
|
PropertyMapError::NotFoundError(err) => {
|
|
|
|
reports.push(
|
|
|
|
Report::build(ReportKind::Error, token.source(), token.start())
|
|
|
|
.with_message("Invalid Graph Property")
|
|
|
|
.with_label(
|
|
|
|
Label::new((
|
|
|
|
token.source().clone(),
|
|
|
|
token.start() + 1..token.end(),
|
|
|
|
))
|
|
|
|
.with_message(format!(
|
|
|
|
"Property `{}` is missing",
|
|
|
|
err.fg(parser.colors().info)
|
|
|
|
))
|
|
|
|
.with_color(parser.colors().warning),
|
|
|
|
)
|
|
|
|
.finish(),
|
|
|
|
);
|
|
|
|
return reports;
|
|
|
|
}
|
|
|
|
_ => panic!("Unknown error")
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2024-07-24 09:09:28 +02:00
|
|
|
// TODO: Caption
|
|
|
|
|
2024-07-24 11:54:04 +02:00
|
|
|
parser.push(
|
|
|
|
document,
|
|
|
|
Box::new(Graphviz {
|
|
|
|
location: token,
|
|
|
|
dot: graph_content,
|
|
|
|
layout: graph_layout,
|
2024-07-24 13:34:00 +02:00
|
|
|
width: graph_width,
|
2024-07-24 11:54:04 +02:00
|
|
|
caption: None,
|
|
|
|
}),
|
|
|
|
);
|
2024-07-24 09:09:28 +02:00
|
|
|
|
|
|
|
reports
|
2024-07-24 11:54:04 +02:00
|
|
|
}
|
2024-07-24 09:09:28 +02:00
|
|
|
|
|
|
|
// TODO
|
|
|
|
fn lua_bindings<'lua>(&self, _lua: &'lua Lua) -> Vec<(String, Function<'lua>)> { vec![] }
|
|
|
|
}
|