nml/src/elements/graphviz.rs

376 lines
9.4 KiB
Rust
Raw Normal View History

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
2024-07-30 11:01:22 +02:00
fn lua_bindings<'lua>(&self, _lua: &'lua Lua) -> Option<Vec<(String, Function<'lua>)>> { None }
2024-07-24 09:09:28 +02:00
}