Compare commits
No commits in common. "7a2c19af66c45911353133b096f08f094e308670" and "554a83a63c6d719bc99698dc6575ac613aaf6729" have entirely different histories.
7a2c19af66
...
554a83a63c
22 changed files with 133 additions and 870 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -737,7 +737,6 @@ dependencies = [
|
|||
"tokio",
|
||||
"tower-lsp",
|
||||
"unicode-segmentation",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -36,4 +36,3 @@ tokio = { version = "1.38.1", features = ["macros", "rt-multi-thread", "io-std"]
|
|||
|
||||
tower-lsp = "0.20.0"
|
||||
unicode-segmentation = "1.11.0"
|
||||
walkdir = "2.5.0"
|
||||
|
|
102
docs/external/latex.nml
vendored
102
docs/external/latex.nml
vendored
|
@ -1,102 +0,0 @@
|
|||
@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...
|
|
@ -1,6 +0,0 @@
|
|||
@import template.nml
|
||||
@compiler.output = index.html
|
||||
@nav.title = Documentation
|
||||
@html.page_title = Documentation | Index
|
||||
|
||||
# Welcome to the NML documentation!
|
|
@ -1,20 +0,0 @@
|
|||
@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" .. "](#)">%
|
|
@ -1,31 +0,0 @@
|
|||
@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**`
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
@import ../template.nml
|
||||
@compiler.output = user-defined.html
|
||||
@nav.title = User-Defined
|
||||
@nav.category = Styles
|
||||
@html.page_title = Documentation | User-Defined Styles
|
||||
|
||||
# TODO
|
|
@ -1,8 +0,0 @@
|
|||
@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,7 +19,6 @@ pub struct Compiler {
|
|||
target: Target,
|
||||
cache: Option<RefCell<Connection>>,
|
||||
reference_count: RefCell<HashMap<String, HashMap<String, usize>>>,
|
||||
// TODO: External references, i.e resolved later
|
||||
}
|
||||
|
||||
impl Compiler {
|
||||
|
@ -74,8 +73,8 @@ impl Compiler {
|
|||
self.cache.as_ref().map(RefCell::borrow_mut)
|
||||
}
|
||||
|
||||
pub fn sanitize<S: AsRef<str>>(target: Target, str: S) -> String {
|
||||
match target {
|
||||
pub fn sanitize<S: AsRef<str>>(&self, str: S) -> String {
|
||||
match self.target {
|
||||
Target::HTML => str
|
||||
.as_ref()
|
||||
.replace("&", "&")
|
||||
|
@ -109,18 +108,18 @@ impl Compiler {
|
|||
result += "<!DOCTYPE HTML><html><head>";
|
||||
result += "<meta charset=\"UTF-8\">";
|
||||
if let Some(page_title) = get_variable_or_error(document, "html.page_title") {
|
||||
result += format!("<title>{}</title>", Compiler::sanitize(self.target(), page_title.to_string()))
|
||||
result += format!("<title>{}</title>", self.sanitize(page_title.to_string()))
|
||||
.as_str();
|
||||
}
|
||||
|
||||
if let Some(css) = document.get_variable("html.css") {
|
||||
result += format!(
|
||||
"<link rel=\"stylesheet\" href=\"{}\">",
|
||||
Compiler::sanitize(self.target(), css.to_string())
|
||||
self.sanitize(css.to_string())
|
||||
)
|
||||
.as_str();
|
||||
}
|
||||
result += r#"</head><body><div id="layout">"#;
|
||||
result += "</head><body>";
|
||||
|
||||
// TODO: TOC
|
||||
// TODO: Author, Date, Title, Div
|
||||
|
@ -134,125 +133,42 @@ impl Compiler {
|
|||
let mut result = String::new();
|
||||
match self.target() {
|
||||
Target::HTML => {
|
||||
result += "</div></body></html>";
|
||||
result += "</body></html>";
|
||||
}
|
||||
Target::LATEX => todo!(""),
|
||||
Target::LATEX => {}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn compile(&self, document: &dyn Document) -> CompiledDocument {
|
||||
pub fn compile(&self, document: &dyn Document) -> String {
|
||||
let mut out = String::new();
|
||||
let borrow = document.content().borrow();
|
||||
|
||||
// Header
|
||||
let header = self.header(document);
|
||||
out += self.header(document).as_str();
|
||||
|
||||
// Body
|
||||
let mut body = r#"<div id="content">"#.to_string();
|
||||
for i in 0..borrow.len() {
|
||||
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) {
|
||||
Ok(result) => body.push_str(result.as_str()),
|
||||
Ok(result) => {
|
||||
//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()),
|
||||
}
|
||||
}
|
||||
body.push_str("</div>");
|
||||
|
||||
// Footer
|
||||
let footer = self.footer(document);
|
||||
out += self.footer(document).as_str();
|
||||
|
||||
// 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,
|
||||
),
|
||||
)
|
||||
out
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
pub mod compiler;
|
||||
pub mod navigation;
|
||||
|
|
|
@ -1,148 +0,0 @@
|
|||
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,6 +91,9 @@ impl Variable for PathVariable
|
|||
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) {
|
||||
// TODO: Avoid copying the content...
|
||||
// Maybe create a special VirtualSource where the `content()` method
|
||||
// calls `Variable::to_string()`
|
||||
let source = Rc::new(VirtualSource::new(
|
||||
location,
|
||||
self.name().to_string(),
|
||||
|
@ -102,3 +105,42 @@ 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 {
|
||||
result += format!(
|
||||
"<div class=\"code-block-title\">{}</div>",
|
||||
Compiler::sanitize(compiler.target(), name.as_str())
|
||||
compiler.sanitize(name.as_str())
|
||||
)
|
||||
.as_str();
|
||||
}
|
||||
|
@ -321,7 +321,7 @@ impl CodeRule {
|
|||
)
|
||||
.unwrap(),
|
||||
Regex::new(
|
||||
r"``(?:\[((?:\\.|[^\\\\])*?)\])?(?:(.*?),)?((?:\\(?:.|\n)|[^\\\\])*?)``",
|
||||
r"``(?:\[((?:\\.|[^\[\]\\])*?)\])?(?:(.*?)(?:\n|,))?((?:\\(?:.|\n)|[^\\\\])*?)``",
|
||||
)
|
||||
.unwrap(),
|
||||
],
|
||||
|
@ -612,13 +612,7 @@ impl RegexRule for CodeRule {
|
|||
bindings.push((
|
||||
"push_block".to_string(),
|
||||
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.as_ref().map(|ctx| {
|
||||
let theme = ctx
|
||||
|
|
|
@ -45,13 +45,13 @@ impl Element for Link {
|
|||
match compiler.target() {
|
||||
Target::HTML => Ok(format!(
|
||||
"<a href=\"{}\">{}</a>",
|
||||
Compiler::sanitize(compiler.target(), self.url.as_str()),
|
||||
Compiler::sanitize(compiler.target(), self.name.as_str()),
|
||||
compiler.sanitize(self.url.as_str()),
|
||||
compiler.sanitize(self.name.as_str()),
|
||||
)),
|
||||
Target::LATEX => Ok(format!(
|
||||
"\\href{{{}}}{{{}}}",
|
||||
Compiler::sanitize(compiler.target(), self.url.as_str()),
|
||||
Compiler::sanitize(compiler.target(), self.name.as_str()),
|
||||
compiler.sanitize(self.url.as_str()),
|
||||
compiler.sanitize(self.name.as_str()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,26 +150,18 @@ impl Element for Medium {
|
|||
.map_or(String::new(), |w| format!(r#" style="width:{w};""#));
|
||||
result.push_str(format!(r#"<div class="medium"{width}>"#).as_str());
|
||||
result += match self.media_type {
|
||||
MediaType::IMAGE => format!(r#"<a href="{0}"><img src="{0}"></a>"#, self.uri),
|
||||
MediaType::VIDEO => format!(
|
||||
r#"<video controls{width}><source src="{0}"></video>"#,
|
||||
self.uri
|
||||
),
|
||||
MediaType::AUDIO => {
|
||||
format!(r#"<audio controls src="{0}"{width}></audio>"#, self.uri)
|
||||
}
|
||||
}
|
||||
.as_str();
|
||||
MediaType::IMAGE =>
|
||||
format!(r#"<a href="{0}"><img src="{0}"></a>"#, self.uri),
|
||||
MediaType::VIDEO =>
|
||||
format!(r#"<video controls{width}><source src="{0}"></video>"#, self.uri),
|
||||
MediaType::AUDIO =>
|
||||
format!(r#"<audio controls src="{0}"{width}></audio>"#, self.uri),
|
||||
}.as_str();
|
||||
|
||||
let caption = self
|
||||
.caption
|
||||
.as_ref()
|
||||
.and_then(|cap| {
|
||||
Some(format!(
|
||||
" {}",
|
||||
Compiler::sanitize(compiler.target(), cap.as_str())
|
||||
))
|
||||
})
|
||||
.and_then(|cap| Some(format!(" {}", compiler.sanitize(cap.as_str()))))
|
||||
.unwrap_or(String::new());
|
||||
|
||||
// Reference
|
||||
|
|
|
@ -41,7 +41,7 @@ impl Element for Section {
|
|||
Target::HTML => Ok(format!(
|
||||
"<h{0}>{1}</h{0}>",
|
||||
self.depth,
|
||||
Compiler::sanitize(compiler.target(), self.title.as_str())
|
||||
compiler.sanitize(self.title.as_str())
|
||||
)),
|
||||
Target::LATEX => Err("Unimplemented compiler".to_string()),
|
||||
}
|
||||
|
|
|
@ -165,6 +165,8 @@ impl Element for Tex {
|
|||
}
|
||||
});
|
||||
|
||||
// TODO: Do something with the caption
|
||||
|
||||
let exec = document
|
||||
.get_variable(format!("tex.{}.exec", self.env).as_str())
|
||||
.map_or("latex2svg".to_string(), |var| var.to_string());
|
||||
|
@ -189,7 +191,7 @@ impl Element for Tex {
|
|||
Tex::format_latex(&fontsize, &preamble, &format!("{prepend}{}", self.tex))
|
||||
};
|
||||
|
||||
let mut result = if let Some(mut con) = compiler.cache() {
|
||||
if let Some(mut con) = compiler.cache() {
|
||||
match latex.cached(&mut con, |s| s.latex_to_svg(&exec, &fontsize)) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => match e {
|
||||
|
@ -201,22 +203,7 @@ impl Element for Tex {
|
|||
}
|
||||
} else {
|
||||
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"),
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ impl Element for Text {
|
|||
fn to_string(&self) -> String { format!("{self:#?}") }
|
||||
|
||||
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> {
|
||||
Ok(Compiler::sanitize(compiler.target(), self.content.as_str()))
|
||||
Ok(compiler.sanitize(self.content.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ pub struct VariableRule {
|
|||
impl VariableRule {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
re: [Regex::new(r"(?:^|\n)@([^[:alpha:]])?(.*?)=((?:\\\n|.)*)").unwrap()],
|
||||
re: [Regex::new(r"(?:^|\n)@([^[:alpha:]])?(.*)=((?:\\\n|.)*)").unwrap()],
|
||||
kinds: vec![
|
||||
("".into(), "Regular".into()),
|
||||
("'".into(), "Path".into())
|
||||
|
@ -89,6 +89,8 @@ impl RegexRule for VariableRule {
|
|||
|
||||
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>)>>
|
||||
{
|
||||
let mut result = vec![];
|
||||
|
|
359
src/main.rs
359
src/main.rs
|
@ -7,29 +7,18 @@ mod lua;
|
|||
mod parser;
|
||||
|
||||
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::time::UNIX_EPOCH;
|
||||
|
||||
use compiler::compiler::CompiledDocument;
|
||||
use compiler::compiler::Compiler;
|
||||
use compiler::compiler::Target;
|
||||
use compiler::navigation::create_navigation;
|
||||
use document::document::Document;
|
||||
use getopts::Options;
|
||||
use parser::langparser::LangParser;
|
||||
use parser::parser::Parser;
|
||||
use rusqlite::Connection;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::parser::source::SourceFile;
|
||||
extern crate getopts;
|
||||
|
||||
fn print_usage(program: &str, opts: Options) {
|
||||
let brief = format!("Usage: {} -i PATH -o PATH [options]", program);
|
||||
let brief = format!("Usage: {} -i FILE [options]", program);
|
||||
print!("{}", opts.usage(&brief));
|
||||
}
|
||||
|
||||
|
@ -47,8 +36,40 @@ NML version: 0.4\n"
|
|||
);
|
||||
}
|
||||
|
||||
fn parse(input: &str, debug_opts: &Vec<String>) -> Result<Box<dyn Document<'static>>, String> {
|
||||
println!("Parsing {input}...");
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
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();
|
||||
|
||||
// Parse
|
||||
|
@ -82,312 +103,12 @@ fn parse(input: &str, debug_opts: &Vec<String>) -> Result<Box<dyn Document<'stat
|
|||
}
|
||||
|
||||
if parser.has_error() {
|
||||
return Err("Parsing failed aborted due to errors while parsing".to_string());
|
||||
println!("Compilation aborted due to errors while parsing");
|
||||
return;
|
||||
}
|
||||
|
||||
Ok(doc)
|
||||
}
|
||||
let compiler = Compiler::new(compiler::compiler::Target::HTML, db_path);
|
||||
let out = compiler.compile(doc.as_ref());
|
||||
|
||||
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;
|
||||
std::fs::write("a.html", out).unwrap();
|
||||
}
|
||||
|
|
|
@ -144,7 +144,7 @@ impl Parser for LangParser {
|
|||
})
|
||||
.unwrap();
|
||||
|
||||
paragraph.push(elem).unwrap();
|
||||
paragraph.push(elem);
|
||||
} else {
|
||||
// Process paragraph events
|
||||
if doc.last_element::<Paragraph>().is_some_and(|_| true) {
|
||||
|
|
72
style.css
72
style.css
|
@ -7,7 +7,6 @@ body {
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Styles */
|
||||
em {
|
||||
padding-left: .1em;
|
||||
padding-right: .1em;
|
||||
|
@ -20,7 +19,9 @@ em {
|
|||
background-color: #191f26;
|
||||
}
|
||||
|
||||
a.inline-code {
|
||||
/* Styles */
|
||||
a.inline-code
|
||||
{
|
||||
padding-left: .1em;
|
||||
padding-right: .1em;
|
||||
|
||||
|
@ -28,73 +29,6 @@ a.inline-code {
|
|||
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 */
|
||||
div.code-block-title {
|
||||
background-color: #20202a;
|
||||
|
|
Loading…
Reference in a new issue