nml/src/elements/graphviz.rs

472 lines
12 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;
2024-08-14 11:09:42 +02:00
use std::sync::Arc;
2024-07-24 11:54:04 +02:00
use std::sync::Once;
2024-08-14 11:09:42 +02:00
use crate::lua::kernel::CTX;
2024-08-05 18:40:17 +02:00
use crate::parser::parser::ParserState;
2024-07-24 11:54:04 +02:00
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;
2024-08-14 11:09:42 +02:00
use mlua::Error::BadArgument;
use mlua::Function;
use mlua::Lua;
2024-07-24 11:54:04 +02:00
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::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 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);
2024-08-06 18:58:41 +02:00
result.push_str(out.split_at(svg_start + split_at).1);
2024-07-24 13:34:00 +02:00
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());
2024-07-31 10:54:19 +02:00
hasher.input(self.width.as_bytes());
2024-07-24 11:54:04 +02:00
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-08-14 11:09:42 +02:00
fn compile(
&self,
compiler: &Compiler,
_document: &dyn Document,
_cursor: usize,
) -> 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(|| {
2024-08-14 11:09:42 +02:00
if let Some(con) = compiler.cache() {
if let Err(e) = Graphviz::init(con) {
2024-07-24 11:54:04 +02:00
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-08-14 11:09:42 +02:00
if let Some(con) = compiler.cache() {
match self.cached(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-08-08 17:11:32 +02:00
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::graphviz")]
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(),
2024-08-06 18:58:41 +02:00
Property::new(true, "SVG width".to_string(), Some("100%".to_string())),
2024-07-24 13:34:00 +02:00
);
2024-07-24 09:09:28 +02:00
Self {
2024-07-24 11:54:04 +02:00
re: [Regex::new(
r"\[graph\](?:\[((?:\\.|[^\[\]\\])*?)\])?(?:((?:\\.|[^\\\\])*?)\[/graph\])?",
)
.unwrap()],
2024-08-06 18:58:41 +02:00
properties: PropertyParser { properties: props },
2024-07-24 09:09:28 +02:00
}
}
}
2024-07-24 11:54:04 +02:00
impl RegexRule for GraphRule {
2024-08-14 11:09:42 +02:00
fn name(&self) -> &'static str { "Graphviz" }
2024-08-08 14:12:16 +02:00
fn previous(&self) -> Option<&'static str> { Some("Tex") }
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,
2024-08-06 18:58:41 +02:00
state: &ParserState,
2024-07-24 11:54:04 +02:00
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 `{}`",
2024-08-05 18:40:17 +02:00
"[/graph]".fg(state.parser.colors().info),
"[graph]".fg(state.parser.colors().info)
2024-07-24 11:54:04 +02:00
))
2024-08-05 18:40:17 +02:00
.with_color(state.parser.colors().error),
2024-07-24 11:54:04 +02:00
)
.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")
2024-08-05 18:40:17 +02:00
.with_color(state.parser.colors().error),
2024-07-24 11:54:04 +02:00
)
.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}"))
2024-08-05 18:40:17 +02:00
.with_color(state.parser.colors().error),
2024-07-24 11:54:04 +02:00
)
.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)
2024-08-05 18:40:17 +02:00
.with_color(state.parser.colors().error),
2024-07-24 11:54:04 +02:00
)
.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: {}",
2024-08-05 18:40:17 +02:00
prop.fg(state.parser.colors().info),
err.fg(state.parser.colors().error)
2024-07-24 11:54:04 +02:00
))
2024-08-05 18:40:17 +02:00
.with_color(state.parser.colors().warning),
2024-07-24 11:54:04 +02:00
)
.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-08-05 18:40:17 +02:00
.with_color(state.parser.colors().warning),
2024-07-24 11:54:04 +02:00
)
.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",
2024-08-05 18:40:17 +02:00
err.fg(state.parser.colors().info)
2024-07-24 13:34:00 +02:00
))
2024-08-05 18:40:17 +02:00
.with_color(state.parser.colors().warning),
2024-07-24 13:34:00 +02:00
)
.finish(),
);
return reports;
}
2024-08-06 18:58:41 +02:00
_ => panic!("Unknown error"),
2024-07-24 13:34:00 +02:00
},
};
2024-08-06 18:58:41 +02:00
state.push(
2024-07-24 11:54:04 +02:00
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
}),
);
2024-07-24 09:09:28 +02:00
reports
2024-07-24 11:54:04 +02:00
}
2024-08-14 11:09:42 +02:00
fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
let mut bindings = vec![];
bindings.push((
"push".to_string(),
lua.create_function(|_, (layout, width, dot): (String, String, String)| {
let mut result = Ok(());
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
let layout = match layout_from_str(layout.as_str()) {
Err(err) => {
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 1,
name: Some("layout".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Unable to get layout type: {err}"
))),
});
return;
}
Ok(layout) => layout,
};
ctx.state.push(
ctx.document,
Box::new(Graphviz {
location: ctx.location.clone(),
dot,
layout,
width,
}),
);
})
});
result
})
.unwrap(),
));
bindings
}
}
#[cfg(test)]
mod tests {
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
use crate::validate_document;
use super::*;
#[test]
pub fn parse() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
[graph][width=200px, layout=neato]
Some graph...
[/graph]
[graph]
Another graph
[/graph]
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Graphviz { width == "200px", dot == "Some graph..." };
Graphviz { dot == "Another graph" };
);
}
#[test]
pub fn lua() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
%<nml.graphviz.push("neato", "200px", "Some graph...")>%
%<nml.graphviz.push("dot", "", "Another graph")>%
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Graphviz { width == "200px", dot == "Some graph..." };
Graphviz { dot == "Another graph" };
);
}
2024-07-24 09:09:28 +02:00
}