nml/src/elements/tex.rs

326 lines
7.2 KiB
Rust
Raw Normal View History

2024-07-24 11:54:04 +02:00
use std::io::Read;
use std::io::Write;
use std::ops::Range;
use std::process::Command;
use std::process::Stdio;
use std::rc::Rc;
use std::sync::Once;
use ariadne::Fmt;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use crypto::digest::Digest;
use crypto::sha2::Sha512;
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-19 11:52:12 +02:00
#[derive(Debug, PartialEq, Eq)]
2024-07-24 11:54:04 +02:00
enum TexKind {
2024-07-19 11:52:12 +02:00
Block,
Inline,
}
2024-07-24 11:54:04 +02:00
impl From<&TexKind> for ElemKind {
fn from(value: &TexKind) -> Self {
2024-07-19 11:52:12 +02:00
match value {
TexKind::Inline => ElemKind::Inline,
2024-07-24 11:54:04 +02:00
_ => ElemKind::Block,
2024-07-19 11:52:12 +02:00
}
2024-07-24 11:54:04 +02:00
}
2024-07-19 11:52:12 +02:00
}
#[derive(Debug)]
2024-07-24 11:54:04 +02:00
struct Tex {
2024-07-19 11:52:12 +02:00
location: Token,
block: TexKind,
env: String,
tex: String,
caption: Option<String>,
}
impl Tex {
2024-07-24 11:54:04 +02:00
fn new(
location: Token,
block: TexKind,
env: String,
tex: String,
caption: Option<String>,
) -> Self {
Self {
location,
block,
env,
tex,
caption,
}
}
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn format_latex(fontsize: &String, preamble: &String, tex: &String) -> FormattedTex {
FormattedTex(format!(
r"\documentclass[{}pt,preview]{{standalone}}
2024-07-19 11:52:12 +02:00
{}
\begin{{document}}
\begin{{preview}}
{}
\end{{preview}}
\end{{document}}",
2024-07-24 11:54:04 +02:00
fontsize, preamble, tex
))
2024-07-19 11:52:12 +02:00
}
}
struct FormattedTex(String);
2024-07-24 11:54:04 +02:00
impl FormattedTex {
2024-07-19 11:52:12 +02:00
/// Renders latex to svg
2024-07-24 11:54:04 +02:00
fn latex_to_svg(&self, exec: &String, fontsize: &String) -> Result<String, String> {
2024-07-19 11:52:12 +02:00
print!("Rendering LaTex `{}`... ", self.0);
let process = match Command::new(exec)
2024-07-24 11:54:04 +02:00
.arg("--fontsize")
.arg(fontsize)
2024-07-19 11:52:12 +02:00
.stdout(Stdio::piped())
.stdin(Stdio::piped())
.spawn()
{
2024-07-24 11:54:04 +02:00
Err(e) => return Err(format!("Could not spawn `{exec}`: {}", e)),
Ok(process) => process,
};
if let Err(e) = process.stdin.unwrap().write_all(self.0.as_bytes()) {
2024-07-19 11:52:12 +02:00
panic!("Unable to write to `latex2svg`'s stdin: {}", e);
}
let mut result = String::new();
2024-07-24 11:54:04 +02:00
match process.stdout.unwrap().read_to_string(&mut result) {
2024-07-19 11:52:12 +02:00
Err(e) => panic!("Unable to read `latex2svg` stdout: {}", e),
Ok(_) => {}
}
println!("Done!");
Ok(result)
}
}
2024-07-24 11:54:04 +02:00
impl Cached for FormattedTex {
type Key = String;
type Value = String;
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn sql_table() -> &'static str {
2024-07-19 11:52:12 +02:00
"CREATE TABLE IF NOT EXISTS cached_tex (
digest TEXT PRIMARY KEY,
svg BLOB NOT NULL);"
2024-07-24 11:54:04 +02:00
}
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn sql_get_query() -> &'static str {
2024-07-19 11:52:12 +02:00
"SELECT svg FROM cached_tex WHERE digest = (?1)"
2024-07-24 11:54:04 +02:00
}
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn sql_insert_query() -> &'static str {
2024-07-19 11:52:12 +02:00
"INSERT INTO cached_tex (digest, svg) VALUES (?1, ?2)"
2024-07-24 11:54:04 +02:00
}
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn key(&self) -> <Self as Cached>::Key {
2024-07-19 11:52:12 +02:00
let mut hasher = Sha512::new();
hasher.input(self.0.as_bytes());
hasher.result_str()
2024-07-24 11:54:04 +02:00
}
2024-07-19 11:52:12 +02:00
}
impl Element for Tex {
2024-07-24 11:54:04 +02:00
fn location(&self) -> &Token {
&self.location
}
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn kind(&self) -> ElemKind {
(&self.block).into()
}
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn element_name(&self) -> &'static str {
"LaTeX"
}
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn to_string(&self) -> String {
format!("{self:#?}")
}
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> {
2024-07-19 11:52:12 +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) = FormattedTex::init(&mut con) {
eprintln!("Unable to create cache table: {e}");
}
2024-07-19 11:52:12 +02:00
}
});
2024-07-24 11:54:04 +02:00
let exec = document
.get_variable(format!("tex.{}.exec", self.env).as_str())
2024-07-23 14:04:57 +02:00
.map_or("latex2svg".to_string(), |var| var.to_string());
2024-07-19 11:52:12 +02:00
// FIXME: Because fontsize is passed as an arg, verify that it cannot be used to execute python/shell code
2024-07-24 11:54:04 +02:00
let fontsize = document
.get_variable(format!("tex.{}.fontsize", self.env).as_str())
2024-07-23 14:04:57 +02:00
.map_or("12".to_string(), |var| var.to_string());
2024-07-24 11:54:04 +02:00
let preamble = document
.get_variable(format!("tex.{}.preamble", self.env).as_str())
2024-07-23 14:04:57 +02:00
.map_or("".to_string(), |var| var.to_string());
2024-07-24 11:54:04 +02:00
let prepend = if self.block == TexKind::Inline {
"".to_string()
} else {
document
.get_variable(format!("tex.{}.block_prepend", self.env).as_str())
.map_or("".to_string(), |var| var.to_string() + "\n")
2024-07-19 11:52:12 +02:00
};
2024-07-24 11:54:04 +02:00
let latex = match self.block {
TexKind::Inline => {
Tex::format_latex(&fontsize, &preamble, &format!("${{{}}}$", self.tex))
}
_ => Tex::format_latex(&fontsize, &preamble, &format!("{prepend}{}", self.tex)),
2024-07-19 11:52:12 +02:00
};
2024-07-24 11:54:04 +02:00
if let Some(mut con) = compiler.cache() {
match latex.cached(&mut con, |s| s.latex_to_svg(&exec, &fontsize)) {
2024-07-19 11:52:12 +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),
},
2024-07-19 11:52:12 +02:00
}
2024-07-24 11:54:04 +02:00
} else {
2024-07-19 11:52:12 +02:00
latex.latex_to_svg(&exec, &fontsize)
}
}
2024-07-24 11:54:04 +02:00
_ => todo!("Unimplemented"),
2024-07-19 11:52:12 +02:00
}
2024-07-24 11:54:04 +02:00
}
2024-07-19 11:52:12 +02:00
}
pub struct TexRule {
re: [Regex; 2],
}
impl TexRule {
pub fn new() -> Self {
Self {
re: [
Regex::new(r"\$\|(?:\[(.*)\])?(?:((?:\\.|[^\\\\])*?)\|\$)?").unwrap(),
Regex::new(r"\$(?:\[(.*)\])?(?:((?:\\.|[^\\\\])*?)\$)?").unwrap(),
],
}
}
}
2024-07-24 11:54:04 +02:00
impl RegexRule for TexRule {
fn name(&self) -> &'static str {
"Tex"
}
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn regexes(&self) -> &[regex::Regex] {
&self.re
}
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
fn on_regex_match(
&self,
index: usize,
parser: &dyn Parser,
document: &dyn Document,
token: Token,
matches: Captures,
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
2024-07-19 11:52:12 +02:00
let mut reports = vec![];
2024-07-24 11:54:04 +02:00
let tex_env = matches
.get(1)
2024-07-19 11:52:12 +02:00
.and_then(|env| Some(env.as_str().trim_start().trim_end()))
.and_then(|env| (!env.is_empty()).then_some(env))
.unwrap_or("main");
2024-07-24 11:54:04 +02:00
let tex_content = match matches.get(2) {
2024-07-19 11:52:12 +02:00
// Unterminated `$`
None => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
2024-07-24 11:54:04 +02:00
.with_message("Unterminated Tex Code")
.with_label(
Label::new((token.source().clone(), token.range.clone()))
.with_message(format!(
"Missing terminating `{}` after first `{}`",
["|$", "$"][index].fg(parser.colors().info),
["$|", "$"][index].fg(parser.colors().info)
))
.with_color(parser.colors().error),
)
.finish(),
);
2024-07-19 11:52:12 +02:00
return reports;
}
Some(content) => {
2024-07-24 11:54:04 +02:00
let processed = util::process_escaped(
'\\',
["|$", "$"][index],
content.as_str().trim_start().trim_end(),
);
2024-07-19 11:52:12 +02:00
2024-07-24 11:54:04 +02:00
if processed.is_empty() {
2024-07-19 11:52:12 +02:00
reports.push(
Report::build(ReportKind::Warning, token.source(), content.start())
2024-07-24 11:54:04 +02:00
.with_message("Empty Tex Code")
.with_label(
Label::new((token.source().clone(), content.range()))
.with_message("Tex code is empty")
.with_color(parser.colors().warning),
)
.finish(),
);
2024-07-19 11:52:12 +02:00
}
processed
}
};
// TODO: Caption
2024-07-24 11:54:04 +02:00
parser.push(
document,
Box::new(Tex::new(
token,
if index == 1 {
TexKind::Inline
} else {
TexKind::Block
},
tex_env.to_string(),
tex_content,
None,
)),
);
2024-07-19 11:52:12 +02:00
reports
2024-07-24 11:54:04 +02:00
}
2024-07-21 15:56:56 +02:00
// TODO
2024-07-24 11:54:04 +02:00
fn lua_bindings<'lua>(&self, _lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
vec![]
}
2024-07-19 11:52:12 +02:00
}