Compare commits

..

8 commits

Author SHA1 Message Date
7a2c19af66 QOL fixes & Tex caption 2024-07-30 09:03:10 +02:00
b252610fbd Fixes & Doc 2024-07-29 21:28:06 +02:00
a777e0ca8f Update layout 2024-07-29 18:06:28 +02:00
30dff576e7 Update style 2024-07-29 18:06:18 +02:00
8721f97b98 Collapse navbar 2024-07-29 16:45:14 +02:00
c8d35a7dc3 Cached building 2024-07-29 13:32:05 +02:00
ac0c4050eb Experimental navigation 2024-07-28 14:20:58 +02:00
c62039dfdf Added batch processing mode 2024-07-27 21:32:12 +02:00
22 changed files with 870 additions and 133 deletions

1
Cargo.lock generated
View file

@ -737,6 +737,7 @@ dependencies = [
"tokio", "tokio",
"tower-lsp", "tower-lsp",
"unicode-segmentation", "unicode-segmentation",
"walkdir",
] ]
[[package]] [[package]]

View file

@ -36,3 +36,4 @@ tokio = { version = "1.38.1", features = ["macros", "rt-multi-thread", "io-std"]
tower-lsp = "0.20.0" tower-lsp = "0.20.0"
unicode-segmentation = "1.11.0" unicode-segmentation = "1.11.0"
walkdir = "2.5.0"

102
docs/external/latex.nml vendored Normal file
View file

@ -0,0 +1,102 @@
@import ../template.nml
@compiler.output = latex.html
@nav.title = LaTeX
@nav.category = External Tools
@html.page_title = Documentation | LaTeX
@LaTeX = $|[kind=inline, caption=LaTeX]\LaTeX|$
*Bring some %LaTeX% unto your document!*
# Inline Math
You can add inline math by enclosing %LaTeX% between two ``$``:
* ``$\lim_{n \to \infty} \Big(1 + \frac{1}{n}\Big)^n = e$`` → $\lim_{n \to \infty} \Big(1 + \frac{1}{n}\Big)^n = e$
* ``$\pi = \sqrt{\sum_{n=1}^\infty \frac{1}{n^2}}$`` → $\pi = \sqrt{\sum_{n=1}^\infty \frac{1}{n^2}}$
You can make the %LaTeX% non inline by specifying `kind=block` in it's property: ``$[kind=block] 1+1=2$`` → $[kind=block] 1+1=2$
*(notice how it's not inside a paragraph)*
# Non Math LaTeX
You can write %LaTeX% outside of %LaTeX%'s math environment, by enclosing your code between ``$|...|$``:
``LaTeX,
$|\begin{tikzpicture}
\begin{axis}
\addplot3[patch,patch refines=3,
shader=faceted interp,
patch type=biquadratic]
table[z expr=x^2-y^2]
{
x y
-2 -2
2 -2
2 2
-2 2
0 -2
2 0
0 2
-2 0
0 0
};
\end{axis}
\end{tikzpicture}|$
``
Gives the following:
$|\begin{tikzpicture}
\begin{axis}
\addplot3[patch,patch refines=3,
shader=faceted interp,
patch type=biquadratic]
table[z expr=x^2-y^2]
{
x y
-2 -2
2 -2
2 2
-2 2
0 -2
2 0
0 2
-2 0
0 0
};
\end{axis}
\end{tikzpicture}|$
# LaTeX environment
You can define multiple %LaTeX% environment, the default being `main`
* ``@tex.env.fontsize`` The fontsize (in pt) specified to `latex2svg` (default: `12`).
* ``@tex.env.preamble`` The preamble prepended to every %LaTeX% code.
* ``@tex.env.block_prepend`` Text to prepend to every non math %LaTeX% code.
* ``@tex.env.exec`` The `latex2svg` executable path, defaults to `latex2svg` (need to be in your `\$PATH`)
Replace ``env`` with the name of the custom environment you wish to define.
Here's a preamble to render %LaTeX% gray:
``
@tex.main.fontsize = 9
@tex.main.preamble = \usepackage{xcolor} \\
\usepgfplotslibrary{patchplots} \\
\definecolor{__color1}{HTML}{d5d5d5} \\
\everymath{\color{__color1}}
@tex.main.block_prepend = \color{__color1}
``
To set the environment you wish to use for a particular %LaTeX% element, set the `env` property:
* ``$[env=main] 1+1 = 2$`` → $[env=main] 1+1 = 2$
* ``$[env=other] 1+1 = 2$`` → $[env=other] 1+1 = 2$
# Properties
* ``env`` The %LaTeX% environment to use, defaults to `main`.
* ``kind`` The display kind of the rendered element:
*- `inline` (default for math mode) displays %LaTeX% as part of the current paragraph.
*- `block` (default for non math mode) display %LaTeX% on it's own line.
* ``caption`` Caption for accessibility, defaults to `none`.
# LaTeX cache
%LaTeX% elements that have been successfully rendered to **svg** are stored in the cache database, to avoid processing them a second time.
Note that this cache is shared between documents, so you don't need to reprocess them if they share the same environment.
They are stored under the table named ``cached_tex``, if you modify the `env` all elements will be reprocessed which may take a while...

6
docs/index.nml Normal file
View file

@ -0,0 +1,6 @@
@import template.nml
@compiler.output = index.html
@nav.title = Documentation
@html.page_title = Documentation | Index
# Welcome to the NML documentation!

20
docs/lua/lua.nml Normal file
View file

@ -0,0 +1,20 @@
@import ../template.nml
@compiler.output = lua.html
@nav.title = Lua
@nav.category = Lua
@html.page_title = Documentation | Lua
# Running lua code
Running lua code is done using the following syntax:
``Lua, %<print("Hello World!")>%``
## Lua to text
To convert the return value of your lua code, append ``"`` at the start of your lua expression:
* ``Lua, %<"return "Hello World">%`` → %<"return "Hello World">%
* ``Lua, %<" "Hello, " .. "World">%`` → %<" "Hello, " .. "World">%
## Parse lua string
Additionnaly, you can output lua to be parsed by the document's parser. To do so, append ``!`` at the start of your lua expression:
* ``Lua, %<!"**" .. "Bold from lua?" .. "**">%`` → %<!"**" .. "Bold from lua?" .. "**">%
* ``Lua, %<!"[" .. "Link from Lua" .. "](#)">%`` → %<!"[" .. "Link from Lua" .. "](#)">%

31
docs/styles/basic.nml Normal file
View file

@ -0,0 +1,31 @@
@import ../template.nml
@compiler.output = basic.html
@nav.title = Basic
@nav.category = Styles
@html.page_title = Documentation | Basic Styles
# Basic styles
## Bold
Enclose text between two ``**`` to render it **bold**!
* ``**Bold text**`` → **Bold text**
* ``**Bold [link](#)**`` → **Bold [link](#)**
## Italic
Enclose text between two ``*`` to render it *italic*!
* ``*Italic text*`` → *Italic text*
* ``**Bold + *Italic***`` → **Bold + *Italic***
## Underline
Enclose text between two ``__`` to render it __underlined__!
* ``__Underlined text__`` → __Underlined text__
* ``__Underline + *Italic*__`` → __Underline + *Italic*__
## Highlighted
Enclose text between two `` ` `` to render it `overlined`!
* `` `Highlighted text` `` → `Highlighted text`
* `` `Highlight + **Bold**` `` → `Highlight + **Bold**`

View file

@ -0,0 +1,7 @@
@import ../template.nml
@compiler.output = user-defined.html
@nav.title = User-Defined
@nav.category = Styles
@html.page_title = Documentation | User-Defined Styles
# TODO

8
docs/template.nml Normal file
View file

@ -0,0 +1,8 @@
@html.css = ../style.css
@tex.main.fontsize = 9
@tex.main.preamble = \usepackage{xcolor, tikz, pgfplots} \\
\usepgfplotslibrary{patchplots} \\
\definecolor{__color1}{HTML}{d5d5d5} \\
\everymath{\color{__color1}\displaystyle}
@tex.main.block_prepend = \color{__color1}

View file

@ -19,6 +19,7 @@ pub struct Compiler {
target: Target, target: Target,
cache: Option<RefCell<Connection>>, cache: Option<RefCell<Connection>>,
reference_count: RefCell<HashMap<String, HashMap<String, usize>>>, reference_count: RefCell<HashMap<String, HashMap<String, usize>>>,
// TODO: External references, i.e resolved later
} }
impl Compiler { impl Compiler {
@ -73,8 +74,8 @@ impl Compiler {
self.cache.as_ref().map(RefCell::borrow_mut) self.cache.as_ref().map(RefCell::borrow_mut)
} }
pub fn sanitize<S: AsRef<str>>(&self, str: S) -> String { pub fn sanitize<S: AsRef<str>>(target: Target, str: S) -> String {
match self.target { match target {
Target::HTML => str Target::HTML => str
.as_ref() .as_ref()
.replace("&", "&amp;") .replace("&", "&amp;")
@ -108,18 +109,18 @@ impl Compiler {
result += "<!DOCTYPE HTML><html><head>"; result += "<!DOCTYPE HTML><html><head>";
result += "<meta charset=\"UTF-8\">"; result += "<meta charset=\"UTF-8\">";
if let Some(page_title) = get_variable_or_error(document, "html.page_title") { if let Some(page_title) = get_variable_or_error(document, "html.page_title") {
result += format!("<title>{}</title>", self.sanitize(page_title.to_string())) result += format!("<title>{}</title>", Compiler::sanitize(self.target(), page_title.to_string()))
.as_str(); .as_str();
} }
if let Some(css) = document.get_variable("html.css") { if let Some(css) = document.get_variable("html.css") {
result += format!( result += format!(
"<link rel=\"stylesheet\" href=\"{}\">", "<link rel=\"stylesheet\" href=\"{}\">",
self.sanitize(css.to_string()) Compiler::sanitize(self.target(), css.to_string())
) )
.as_str(); .as_str();
} }
result += "</head><body>"; result += r#"</head><body><div id="layout">"#;
// TODO: TOC // TODO: TOC
// TODO: Author, Date, Title, Div // TODO: Author, Date, Title, Div
@ -133,42 +134,125 @@ impl Compiler {
let mut result = String::new(); let mut result = String::new();
match self.target() { match self.target() {
Target::HTML => { Target::HTML => {
result += "</body></html>"; result += "</div></body></html>";
} }
Target::LATEX => {} Target::LATEX => todo!(""),
} }
result result
} }
pub fn compile(&self, document: &dyn Document) -> String { pub fn compile(&self, document: &dyn Document) -> CompiledDocument {
let mut out = String::new();
let borrow = document.content().borrow(); let borrow = document.content().borrow();
// Header // Header
out += self.header(document).as_str(); let header = self.header(document);
// Body // Body
let mut body = r#"<div id="content">"#.to_string();
for i in 0..borrow.len() { for i in 0..borrow.len() {
let elem = &borrow[i]; let elem = &borrow[i];
//let prev = match i
//{
// 0 => None,
// _ => borrow.get(i-1),
//};
//let next = borrow.get(i+1);
match elem.compile(self, document) { match elem.compile(self, document) {
Ok(result) => { Ok(result) => body.push_str(result.as_str()),
//println!("Elem: {}\nCompiled to: {result}", elem.to_string());
out.push_str(result.as_str())
}
Err(err) => println!("Unable to compile element: {err}\n{}", elem.to_string()), Err(err) => println!("Unable to compile element: {err}\n{}", elem.to_string()),
} }
} }
body.push_str("</div>");
// Footer // Footer
out += self.footer(document).as_str(); let footer = self.footer(document);
out // Variables
let variables = document
.scope()
.borrow_mut()
.variables
.iter()
.map(|(key, var)| (key.clone(), var.to_string()))
.collect::<HashMap<String, String>>();
CompiledDocument {
input: document.source().name().clone(),
mtime: 0,
variables,
header,
body,
footer,
}
}
}
#[derive(Debug)]
pub struct CompiledDocument {
/// Input path relative to the input directory
pub input: String,
/// Modification time (i.e seconds since last epoch)
pub mtime: u64,
// TODO: Also store exported references
// so they can be referenced from elsewhere
// This will also require rebuilding in case some exported references have changed...
/// Variables exported to string, so they can be querried later
pub variables: HashMap<String, String>,
/// Compiled document's header
pub header: String,
/// Compiled document's body
pub body: String,
/// Compiled document's footer
pub footer: String,
}
impl CompiledDocument {
pub fn get_variable(&self, name: &str) -> Option<&String> { self.variables.get(name) }
fn sql_table() -> &'static str {
"CREATE TABLE IF NOT EXISTS compiled_documents (
input TEXT PRIMARY KEY,
mtime INTEGER NOT NULL,
variables TEXT NOT NULL,
header TEXT NOT NULL,
body TEXT NOT NULL,
footer TEXT NOT NULL
);"
}
fn sql_get_query() -> &'static str { "SELECT * FROM compiled_documents WHERE input = (?1)" }
fn sql_insert_query() -> &'static str {
"INSERT OR REPLACE INTO compiled_documents (input, mtime, variables, header, body, footer) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
}
pub fn init_cache(con: &Connection) -> Result<usize, rusqlite::Error> {
con.execute(Self::sql_table(), [])
}
pub fn from_cache(con: &Connection, input: &str) -> Option<Self> {
con.query_row(Self::sql_get_query(), [input], |row| {
Ok(CompiledDocument {
input: input.to_string(),
mtime: row.get_unwrap::<_, u64>(1),
variables: serde_json::from_str(row.get_unwrap::<_, String>(2).as_str()).unwrap(),
header: row.get_unwrap::<_, String>(3),
body: row.get_unwrap::<_, String>(4),
footer: row.get_unwrap::<_, String>(5),
})
})
.ok()
}
/// Inserts [`CompiledDocument`] into cache
pub fn insert_cache(&self, con: &Connection) -> Result<usize, rusqlite::Error> {
con.execute(
Self::sql_insert_query(),
(
&self.input,
&self.mtime,
serde_json::to_string(&self.variables).unwrap(),
&self.header,
&self.body,
&self.footer,
),
)
} }
} }

View file

@ -1 +1,2 @@
pub mod compiler; pub mod compiler;
pub mod navigation;

148
src/compiler/navigation.rs Normal file
View file

@ -0,0 +1,148 @@
use std::collections::HashMap;
use crate::compiler::compiler::Compiler;
use super::compiler::CompiledDocument;
use super::compiler::Target;
#[derive(Debug, Default)]
pub struct NavEntry {
pub(self) entries: Vec<(String, String)>,
pub(self) children: HashMap<String, NavEntry>,
}
impl NavEntry {
// FIXME: Sanitize
pub fn compile(&self, target: Target, doc: &CompiledDocument) -> String {
let categories = vec![
doc.get_variable("nav.category").map_or("", |s| s.as_str()),
doc.get_variable("nav.subcategory")
.map_or("", |s| s.as_str()),
];
let mut result = String::new();
match target {
Target::HTML => {
result += r#"<div id="navbar"><ul>"#;
fn process(
target: Target,
categories: &Vec<&str>,
did_match: bool,
result: &mut String,
entry: &NavEntry,
depth: usize,
) {
// Orphans = Links
for (title, path) in &entry.entries {
result.push_str(
format!(
r#"<li><a href="{}">{}</a></li>"#,
Compiler::sanitize(target, path),
Compiler::sanitize(target, title)
)
.as_str(),
);
}
// Recurse
for (name, ent) in &entry.children {
let is_match = if did_match {
categories.get(depth) == Some(&name.as_str())
} else {
false || depth == 0
};
result.push_str("<li>");
result.push_str(
format!(
"<details{}><summary>{}</summary>",
["", " open"][is_match as usize],
Compiler::sanitize(target, name)
)
.as_str(),
);
result.push_str("<ul>");
process(target, categories, is_match, result, ent, depth + 1);
result.push_str("</ul></details></li>");
}
}
process(target, &categories, true, &mut result, self, 0);
result += r#"</ul></div>"#;
}
_ => todo!(""),
}
result
}
}
pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, String> {
let mut nav = NavEntry {
entries: vec![],
children: HashMap::new(),
};
for doc in docs {
let cat = doc.get_variable("nav.category");
let subcat = doc.get_variable("nav.subcategory");
let title = doc
.get_variable("nav.title")
.or(doc.get_variable("doc.title"));
let path = doc.get_variable("compiler.output");
let (title, path) = match (title, path) {
(Some(title), Some(path)) => (title, path),
_ => {
eprintln!("Skipping navigation generation for `{}`, must have a defined `@nav.title` and `@compiler.output`", doc.input);
continue;
}
};
let pent = if let Some(subcat) = subcat {
let cat = match cat {
Some(cat) => cat,
None => {
eprintln!(
"Skipping `{}`: No `@nav.category`, but `@nav.subcategory` is set",
doc.input
);
continue;
}
};
let mut cat_ent = match nav.children.get_mut(cat.as_str()) {
Some(cat_ent) => cat_ent,
None => {
// Insert
nav.children.insert(cat.clone(), NavEntry::default());
nav.children.get_mut(cat.as_str()).unwrap()
}
};
match cat_ent.children.get_mut(subcat.as_str()) {
Some(subcat_ent) => subcat_ent,
None => {
// Insert
cat_ent.children.insert(subcat.clone(), NavEntry::default());
cat_ent.children.get_mut(subcat.as_str()).unwrap()
}
}
} else if let Some(cat) = cat {
match nav.children.get_mut(cat.as_str()) {
Some(cat_ent) => cat_ent,
None => {
// Insert
nav.children.insert(cat.clone(), NavEntry::default());
nav.children.get_mut(cat.as_str()).unwrap()
}
}
} else {
&mut nav
};
pent.entries.push((title.clone(), path.clone()))
}
Ok(nav)
}

View file

@ -91,9 +91,6 @@ impl Variable for PathVariable
fn to_string(&self) -> String { self.path.to_str().unwrap().to_string() } fn to_string(&self) -> String { self.path.to_str().unwrap().to_string() }
fn parse<'a>(&self, location: Token, parser: &dyn Parser, document: &'a dyn Document) { fn parse<'a>(&self, location: Token, parser: &dyn Parser, document: &'a dyn Document) {
// TODO: Avoid copying the content...
// Maybe create a special VirtualSource where the `content()` method
// calls `Variable::to_string()`
let source = Rc::new(VirtualSource::new( let source = Rc::new(VirtualSource::new(
location, location,
self.name().to_string(), self.name().to_string(),
@ -105,42 +102,3 @@ impl Variable for PathVariable
))); )));
} }
} }
/*
struct ConfigVariable<T>
{
value: T,
name: String,
desc: String,
validator: Box<dyn Fn(&Self, &T) -> Option<&String>>,
}
impl<T> ConfigVariable<T>
{
fn description(&self) -> &String { &self.desc }
}
impl<T> Variable for ConfigVariable<T>
where T: FromStr + Display
{
fn name(&self) -> &str { self.name.as_str() }
/// Parse variable from string, returns an error message on failure
fn from_string(&mut self, str: &str) -> Option<String> {
match str.parse::<T>()
{
Ok(value) => {
(self.validator)(self, &value).or_else(|| {
self.value = value;
None
})
},
Err(_) => return Some(format!("Unable to parse `{str}` into variable `{}`", self.name))
}
}
/// Converts variable to a string
fn to_string(&self) -> String { self.value.to_string() }
}
*/

View file

@ -117,7 +117,7 @@ impl Code {
if let Some(name) = &self.name { if let Some(name) = &self.name {
result += format!( result += format!(
"<div class=\"code-block-title\">{}</div>", "<div class=\"code-block-title\">{}</div>",
compiler.sanitize(name.as_str()) Compiler::sanitize(compiler.target(), name.as_str())
) )
.as_str(); .as_str();
} }
@ -321,7 +321,7 @@ impl CodeRule {
) )
.unwrap(), .unwrap(),
Regex::new( Regex::new(
r"``(?:\[((?:\\.|[^\[\]\\])*?)\])?(?:(.*?)(?:\n|,))?((?:\\(?:.|\n)|[^\\\\])*?)``", r"``(?:\[((?:\\.|[^\\\\])*?)\])?(?:(.*?),)?((?:\\(?:.|\n)|[^\\\\])*?)``",
) )
.unwrap(), .unwrap(),
], ],
@ -612,7 +612,13 @@ impl RegexRule for CodeRule {
bindings.push(( bindings.push((
"push_block".to_string(), "push_block".to_string(),
lua.create_function( lua.create_function(
|_, (language, name, content, line_offset): (String, Option<String>, String, Option<usize>)| { |_,
(language, name, content, line_offset): (
String,
Option<String>,
String,
Option<usize>,
)| {
CTX.with_borrow(|ctx| { CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| { ctx.as_ref().map(|ctx| {
let theme = ctx let theme = ctx

View file

@ -45,13 +45,13 @@ impl Element for Link {
match compiler.target() { match compiler.target() {
Target::HTML => Ok(format!( Target::HTML => Ok(format!(
"<a href=\"{}\">{}</a>", "<a href=\"{}\">{}</a>",
compiler.sanitize(self.url.as_str()), Compiler::sanitize(compiler.target(), self.url.as_str()),
compiler.sanitize(self.name.as_str()), Compiler::sanitize(compiler.target(), self.name.as_str()),
)), )),
Target::LATEX => Ok(format!( Target::LATEX => Ok(format!(
"\\href{{{}}}{{{}}}", "\\href{{{}}}{{{}}}",
compiler.sanitize(self.url.as_str()), Compiler::sanitize(compiler.target(), self.url.as_str()),
compiler.sanitize(self.name.as_str()), Compiler::sanitize(compiler.target(), self.name.as_str()),
)), )),
} }
} }

View file

@ -150,18 +150,26 @@ impl Element for Medium {
.map_or(String::new(), |w| format!(r#" style="width:{w};""#)); .map_or(String::new(), |w| format!(r#" style="width:{w};""#));
result.push_str(format!(r#"<div class="medium"{width}>"#).as_str()); result.push_str(format!(r#"<div class="medium"{width}>"#).as_str());
result += match self.media_type { result += match self.media_type {
MediaType::IMAGE => MediaType::IMAGE => format!(r#"<a href="{0}"><img src="{0}"></a>"#, self.uri),
format!(r#"<a href="{0}"><img src="{0}"></a>"#, self.uri), MediaType::VIDEO => format!(
MediaType::VIDEO => r#"<video controls{width}><source src="{0}"></video>"#,
format!(r#"<video controls{width}><source src="{0}"></video>"#, self.uri), self.uri
MediaType::AUDIO => ),
format!(r#"<audio controls src="{0}"{width}></audio>"#, self.uri), MediaType::AUDIO => {
}.as_str(); format!(r#"<audio controls src="{0}"{width}></audio>"#, self.uri)
}
}
.as_str();
let caption = self let caption = self
.caption .caption
.as_ref() .as_ref()
.and_then(|cap| Some(format!(" {}", compiler.sanitize(cap.as_str())))) .and_then(|cap| {
Some(format!(
" {}",
Compiler::sanitize(compiler.target(), cap.as_str())
))
})
.unwrap_or(String::new()); .unwrap_or(String::new());
// Reference // Reference

View file

@ -41,7 +41,7 @@ impl Element for Section {
Target::HTML => Ok(format!( Target::HTML => Ok(format!(
"<h{0}>{1}</h{0}>", "<h{0}>{1}</h{0}>",
self.depth, self.depth,
compiler.sanitize(self.title.as_str()) Compiler::sanitize(compiler.target(), self.title.as_str())
)), )),
Target::LATEX => Err("Unimplemented compiler".to_string()), Target::LATEX => Err("Unimplemented compiler".to_string()),
} }

View file

@ -165,8 +165,6 @@ impl Element for Tex {
} }
}); });
// TODO: Do something with the caption
let exec = document let exec = document
.get_variable(format!("tex.{}.exec", self.env).as_str()) .get_variable(format!("tex.{}.exec", self.env).as_str())
.map_or("latex2svg".to_string(), |var| var.to_string()); .map_or("latex2svg".to_string(), |var| var.to_string());
@ -191,7 +189,7 @@ impl Element for Tex {
Tex::format_latex(&fontsize, &preamble, &format!("{prepend}{}", self.tex)) Tex::format_latex(&fontsize, &preamble, &format!("{prepend}{}", self.tex))
}; };
if let Some(mut con) = compiler.cache() { let mut result = if let Some(mut con) = compiler.cache() {
match latex.cached(&mut con, |s| s.latex_to_svg(&exec, &fontsize)) { match latex.cached(&mut con, |s| s.latex_to_svg(&exec, &fontsize)) {
Ok(s) => Ok(s), Ok(s) => Ok(s),
Err(e) => match e { Err(e) => match e {
@ -203,7 +201,22 @@ impl Element for Tex {
} }
} else { } else {
latex.latex_to_svg(&exec, &fontsize) latex.latex_to_svg(&exec, &fontsize)
};
// Caption
result.map(|mut result| {
if let (Some(caption), Some(start)) = (&self.caption, result.find('>')) {
result.insert_str(
start + 1,
format!(
"<title>{}</title>",
Compiler::sanitize(Target::HTML, caption)
)
.as_str(),
);
} }
result
})
} }
_ => todo!("Unimplemented"), _ => todo!("Unimplemented"),
} }

View file

@ -39,7 +39,7 @@ impl Element for Text {
fn to_string(&self) -> String { format!("{self:#?}") } fn to_string(&self) -> String { format!("{self:#?}") }
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> {
Ok(compiler.sanitize(self.content.as_str())) Ok(Compiler::sanitize(compiler.target(), self.content.as_str()))
} }
} }

View file

@ -13,7 +13,7 @@ pub struct VariableRule {
impl VariableRule { impl VariableRule {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
re: [Regex::new(r"(?:^|\n)@([^[:alpha:]])?(.*)=((?:\\\n|.)*)").unwrap()], re: [Regex::new(r"(?:^|\n)@([^[:alpha:]])?(.*?)=((?:\\\n|.)*)").unwrap()],
kinds: vec![ kinds: vec![
("".into(), "Regular".into()), ("".into(), "Regular".into()),
("'".into(), "Path".into()) ("'".into(), "Path".into())
@ -89,8 +89,6 @@ impl RegexRule for VariableRule {
fn regexes(&self) -> &[Regex] { &self.re } fn regexes(&self) -> &[Regex] { &self.re }
fn on_regex_match<'a>(&self, _: usize, parser: &dyn Parser, document: &'a dyn Document, token: Token, matches: regex::Captures) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> fn on_regex_match<'a>(&self, _: usize, parser: &dyn Parser, document: &'a dyn Document, token: Token, matches: regex::Captures) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>>
{ {
let mut result = vec![]; let mut result = vec![];

View file

@ -7,18 +7,29 @@ mod lua;
mod parser; mod parser;
use std::env; use std::env;
use std::io::BufWriter;
use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;
use std::rc::Rc; use std::rc::Rc;
use std::time::UNIX_EPOCH;
use compiler::compiler::CompiledDocument;
use compiler::compiler::Compiler; use compiler::compiler::Compiler;
use compiler::compiler::Target;
use compiler::navigation::create_navigation;
use document::document::Document;
use getopts::Options; use getopts::Options;
use parser::langparser::LangParser; use parser::langparser::LangParser;
use parser::parser::Parser; use parser::parser::Parser;
use rusqlite::Connection;
use walkdir::WalkDir;
use crate::parser::source::SourceFile; use crate::parser::source::SourceFile;
extern crate getopts; extern crate getopts;
fn print_usage(program: &str, opts: Options) { fn print_usage(program: &str, opts: Options) {
let brief = format!("Usage: {} -i FILE [options]", program); let brief = format!("Usage: {} -i PATH -o PATH [options]", program);
print!("{}", opts.usage(&brief)); print!("{}", opts.usage(&brief));
} }
@ -36,40 +47,8 @@ NML version: 0.4\n"
); );
} }
fn main() { fn parse(input: &str, debug_opts: &Vec<String>) -> Result<Box<dyn Document<'static>>, String> {
let args: Vec<String> = env::args().collect(); println!("Parsing {input}...");
let program = args[0].clone();
let mut opts = Options::new();
opts.optopt("i", "", "Input file", "FILE");
opts.optopt("d", "database", "Cache database location", "PATH");
opts.optmulti("z", "debug", "Debug options", "OPTS");
opts.optflag("h", "help", "Print this help menu");
opts.optflag("v", "version", "Print program version and licenses");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => {
panic!("{}", f.to_string())
}
};
if matches.opt_present("v") {
print_version();
return;
}
if matches.opt_present("h") {
print_usage(&program, opts);
return;
}
if !matches.opt_present("i") {
print_usage(&program, opts);
return;
}
let input = matches.opt_str("i").unwrap();
let debug_opts = matches.opt_strs("z");
let db_path = matches.opt_str("d");
let parser = LangParser::default(); let parser = LangParser::default();
// Parse // Parse
@ -103,12 +82,312 @@ fn main() {
} }
if parser.has_error() { if parser.has_error() {
println!("Compilation aborted due to errors while parsing"); return Err("Parsing failed aborted due to errors while parsing".to_string());
return;
} }
let compiler = Compiler::new(compiler::compiler::Target::HTML, db_path); Ok(doc)
let out = compiler.compile(doc.as_ref()); }
std::fs::write("a.html", out).unwrap(); fn process(
target: Target,
files: Vec<PathBuf>,
db_path: &Option<String>,
force_rebuild: bool,
debug_opts: &Vec<String>,
) -> Result<Vec<CompiledDocument>, String> {
let mut compiled = vec![];
let current_dir = std::env::current_dir()
.map_err(|err| format!("Unable to get the current working directory: {err}"))?;
let con = db_path
.as_ref()
.map_or(Connection::open_in_memory(), |path| Connection::open(path))
.map_err(|err| format!("Unable to open connection to the database: {err}"))?;
CompiledDocument::init_cache(&con)
.map_err(|err| format!("Failed to initialize cached document table: {err}"))?;
for file in files {
let meta = std::fs::metadata(&file)
.map_err(|err| format!("Failed to get metadata for `{file:#?}`: {err}"))?;
let modified = meta
.modified()
.map_err(|err| format!("Unable to query modification time for `{file:#?}`: {err}"))?;
// Move to file's directory
let file_parent_path = file
.parent()
.ok_or(format!("Failed to get parent path for `{file:#?}`"))?;
std::env::set_current_dir(file_parent_path)
.map_err(|err| format!("Failed to move to path `{file_parent_path:#?}`: {err}"))?;
let parse_and_compile = || -> Result<CompiledDocument, String> {
// Parse
let doc = parse(file.to_str().unwrap(), debug_opts)?;
// Compile
let compiler = Compiler::new(target, db_path.clone());
let mut compiled = compiler.compile(&*doc);
// Insert into cache
compiled.mtime = modified.duration_since(UNIX_EPOCH).unwrap().as_secs();
compiled.insert_cache(&con).map_err(|err| {
format!("Failed to insert compiled document from `{file:#?}` into cache: {err}")
})?;
Ok(compiled)
};
let cdoc = if force_rebuild {
parse_and_compile()?
} else {
match CompiledDocument::from_cache(&con, file.to_str().unwrap()) {
Some(compiled) => {
if compiled.mtime < modified.duration_since(UNIX_EPOCH).unwrap().as_secs() {
parse_and_compile()?
} else {
compiled
}
}
None => parse_and_compile()?,
}
};
compiled.push(cdoc);
}
std::env::set_current_dir(current_dir)
.map_err(|err| format!("Failed to set current directory: {err}"))?;
Ok(compiled)
}
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
let program = args[0].clone();
let mut opts = Options::new();
opts.optopt("i", "input", "Input path", "PATH");
opts.optopt("o", "output", "Output path", "PATH");
opts.optopt("d", "database", "Cache database location", "PATH");
opts.optflag("", "force-rebuild", "Force rebuilding of cached documents");
opts.optmulti("z", "debug", "Debug options", "OPTS");
opts.optflag("h", "help", "Print this help menu");
opts.optflag("v", "version", "Print program version and licenses");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => {
panic!("{}", f.to_string())
}
};
if matches.opt_present("v") {
print_version();
return ExitCode::SUCCESS;
}
if matches.opt_present("h") {
print_usage(&program, opts);
return ExitCode::SUCCESS;
}
if !matches.opt_present("i") || !matches.opt_present("o") {
print_usage(&program, opts);
return ExitCode::FAILURE;
}
let input = matches.opt_str("i").unwrap();
let input_meta = match std::fs::metadata(&input) {
Ok(meta) => meta,
Err(e) => {
eprintln!("Unable to get metadata for input: `{input}`");
return ExitCode::FAILURE;
}
};
let output = matches.opt_str("o").unwrap();
if input_meta.is_dir() {
// Create ouput directories
if !std::fs::exists(&output).unwrap_or(false) {
match std::fs::create_dir_all(&output) {
Ok(()) => {}
Err(err) => {
eprintln!("Unable to create output directory `{output}`: {err}");
return ExitCode::FAILURE;
}
}
}
match std::fs::metadata(&output) {
Ok(_) => {}
Err(e) => {
eprintln!("Unable to get metadata for output: `{output}`");
return ExitCode::FAILURE;
}
}
} else if std::fs::exists(&output).unwrap_or(false) {
let output_meta = match std::fs::metadata(&output) {
Ok(meta) => meta,
Err(e) => {
eprintln!("Unable to get metadata for output: `{output}`");
return ExitCode::FAILURE;
}
};
if output_meta.is_dir() {
eprintln!("Input `{input}` is a file, but output `{output}` is a directory");
return ExitCode::FAILURE;
}
}
let db_path = match matches.opt_str("d") {
Some(db) => {
if std::fs::exists(&db).unwrap_or(false) {
match std::fs::canonicalize(&db)
.map_err(|err| format!("Failed to cannonicalize database path `{db}`: {err}"))
.as_ref()
.map(|path| path.to_str())
{
Ok(Some(path)) => Some(path.to_string()),
Ok(None) => {
eprintln!("Failed to transform path to string `{db}`");
return ExitCode::FAILURE;
}
Err(err) => {
eprintln!("{err}");
return ExitCode::FAILURE;
}
}
} else
// Cannonicalize parent path, then append the database name
{
match std::fs::canonicalize(".")
.map_err(|err| {
format!("Failed to cannonicalize database parent path `{db}`: {err}")
})
.map(|path| path.join(&db))
.as_ref()
.map(|path| path.to_str())
{
Ok(Some(path)) => Some(path.to_string()),
Ok(None) => {
eprintln!("Failed to transform path to string `{db}`");
return ExitCode::FAILURE;
}
Err(err) => {
eprintln!("{err}");
return ExitCode::FAILURE;
}
}
}
}
None => None,
};
let force_rebuild = matches.opt_present("force-rebuild");
let debug_opts = matches.opt_strs("z");
let mut files = vec![];
if input_meta.is_dir() {
if db_path.is_none() {
eprintln!("Directory mode requires a database (-d)");
return ExitCode::FAILURE;
}
for entry in WalkDir::new(&input) {
if let Err(err) = entry {
eprintln!("Failed to recursively walk over input directory: {err}");
return ExitCode::FAILURE;
}
match entry.as_ref().unwrap().metadata() {
Ok(meta) => {
if !meta.is_file() {
continue;
}
}
Err(e) => {
eprintln!("Faield to get metadata for `{entry:#?}`");
return ExitCode::FAILURE;
}
}
let path = match entry.as_ref().unwrap().path().to_str() {
Some(path) => path.to_string(),
None => {
eprintln!("Faield to convert input file `{entry:#?}` to UTF-8");
return ExitCode::FAILURE;
}
};
if !path.ends_with(".nml") {
println!("Skipping '{path}'");
continue;
}
files.push(std::fs::canonicalize(path).unwrap());
}
} else {
// Single file mode
files.push(std::fs::canonicalize(input).unwrap());
}
// Check that all files have a valid unicode path
for file in &files {
if file.to_str().is_none() {
eprintln!("Invalid unicode for file: `{file:#?}`");
return ExitCode::FAILURE;
}
}
// Parse, compile using the cache
let compiled = match process(Target::HTML, files, &db_path, force_rebuild, &debug_opts) {
Ok(compiled) => compiled,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
if input_meta.is_dir()
// Batch mode
{
// Build navigation
let navigation = match create_navigation(&compiled) {
Ok(nav) => nav,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
// Output
for doc in compiled {
let out_path = match doc
.get_variable("compiler.output")
.or(input_meta.is_file().then_some(&output))
{
Some(path) => path.clone(),
None => {
eprintln!("Unable to get output file for `{}`", doc.input);
continue;
}
};
let nav = navigation.compile(Target::HTML, &doc);
let file = std::fs::File::create(output.clone() + "/" + out_path.as_str()).unwrap();
let mut writer = BufWriter::new(file);
write!(writer, "{}{}{}{}", doc.header, nav, doc.body, doc.footer).unwrap();
writer.flush().unwrap();
}
} else
// Single file
{
for doc in compiled {
let file = std::fs::File::create(output.clone()).unwrap();
let mut writer = BufWriter::new(file);
write!(writer, "{}{}{}", doc.header, doc.body, doc.footer).unwrap();
writer.flush().unwrap();
}
}
return ExitCode::SUCCESS;
} }

View file

@ -144,7 +144,7 @@ impl Parser for LangParser {
}) })
.unwrap(); .unwrap();
paragraph.push(elem); paragraph.push(elem).unwrap();
} else { } else {
// Process paragraph events // Process paragraph events
if doc.last_element::<Paragraph>().is_some_and(|_| true) { if doc.last_element::<Paragraph>().is_some_and(|_| true) {

View file

@ -7,6 +7,7 @@ body {
margin: 0 auto; margin: 0 auto;
} }
/* Styles */
em { em {
padding-left: .1em; padding-left: .1em;
padding-right: .1em; padding-right: .1em;
@ -19,9 +20,7 @@ em {
background-color: #191f26; background-color: #191f26;
} }
/* Styles */ a.inline-code {
a.inline-code
{
padding-left: .1em; padding-left: .1em;
padding-right: .1em; padding-right: .1em;
@ -29,6 +28,73 @@ a.inline-code
background-color: #191f26; background-color: #191f26;
} }
/* Navbar */
#navbar {
left: 0;
top: 0;
bottom: 0;
width: max(16vw, 20ch);
overflow-y: auto;
position: absolute;
box-sizing: border-box;
overscroll-behavior-y: contain;
background-color: #161a26;
color: #aaa;
font-size: 0.9em;
font-weight: bold;
}
#navbar a {
color: #ffb454;
text-decoration: none;
font-weight: normal;
}
#navbar li {
display: block;
position: relative;
padding-left: 1em;
margin-left: 0em;
}
#navbar ul {
margin-left: 0em;
padding-left: 0;
}
#navbar summary{
display: block;
cursor: pointer;
}
#navbar summary::marker,
#navbar summary::-webkit-details-marker{
display: none;
}
#navbar summary:focus{
outline: none;
}
#navbar summary:focus-visible{
outline: 1px dotted #000;
}
#navbar summary:before {
content: "+";
color: #ffb454;
float: left;
text-align: center;
width: 1em;
}
#navbar details[open] > summary:before {
content: "";
}
/* Code blocks */ /* Code blocks */
div.code-block-title { div.code-block-title {
background-color: #20202a; background-color: #20202a;