Compare commits
8 commits
554a83a63c
...
7a2c19af66
Author | SHA1 | Date | |
---|---|---|---|
7a2c19af66 | |||
b252610fbd | |||
a777e0ca8f | |||
30dff576e7 | |||
8721f97b98 | |||
c8d35a7dc3 | |||
ac0c4050eb | |||
c62039dfdf |
22 changed files with 870 additions and 133 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -737,6 +737,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-lsp",
|
"tower-lsp",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -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
102
docs/external/latex.nml
vendored
Normal 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
6
docs/index.nml
Normal 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
20
docs/lua/lua.nml
Normal 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
31
docs/styles/basic.nml
Normal 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**`
|
||||||
|
|
7
docs/styles/user-defined.nml
Normal file
7
docs/styles/user-defined.nml
Normal 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
8
docs/template.nml
Normal 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}
|
|
@ -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("&", "&")
|
.replace("&", "&")
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
pub mod compiler;
|
pub mod compiler;
|
||||||
|
pub mod navigation;
|
||||||
|
|
148
src/compiler/navigation.rs
Normal file
148
src/compiler/navigation.rs
Normal 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)
|
||||||
|
}
|
|
@ -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() }
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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![];
|
||||||
|
|
361
src/main.rs
361
src/main.rs
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
72
style.css
72
style.css
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue