Add configuration options

This commit is contained in:
Juno Takano 2025-12-16 19:02:22 -03:00
commit 270fed54f0
16 changed files with 272 additions and 83 deletions

View file

@ -3,7 +3,7 @@ use axum::{
http::{Response, StatusCode, header}, http::{Response, StatusCode, header},
}; };
use crate::handlers; use crate::{formats::populate_graph, handlers};
pub(in crate::handlers) fn by_code( pub(in crate::handlers) fn by_code(
code: Option<u16>, code: Option<u16>,
@ -26,6 +26,7 @@ fn make_body(code: Option<u16>, message: Option<&str>) -> String {
let out_code = code.unwrap_or(500); let out_code = code.unwrap_or(500);
let out_message = &message.unwrap_or("Unknown error"); let out_message = &message.unwrap_or("Unknown error");
let config = populate_graph().meta.config;
context.insert( context.insert(
"title", "title",
@ -36,6 +37,7 @@ fn make_body(code: Option<u16>, message: Option<&str>) -> String {
context.insert("message", out_message); context.insert("message", out_message);
context.insert("status_code", &out_code.to_string()); context.insert("status_code", &out_code.to_string());
context.insert("config", &config);
handlers::template::render( handlers::template::render(
"error.html", "error.html",

View file

@ -1,25 +1,25 @@
use axum::{body::Body, extract::Path, http::Response}; use axum::{body::Body, extract::Path, http::Response};
use crate::syntax::content::elements::paragraph::Paragraph;
use crate::syntax::content::parser; use crate::syntax::content::parser;
use crate::{formats::populate_graph, handlers, types::Node}; use crate::{formats::populate_graph, handlers, types::Node};
pub async fn node(Path(id): Path<String>) -> Response<Body> { pub async fn node(Path(id): Path<String>) -> Response<Body> {
let mut context = tera::Context::new();
let graph = populate_graph(); let graph = populate_graph();
let empty_node = Node::new(Some(format!("Could not find node ID {id}."))); let empty_node = Node::new(Some(format!("Could not find node ID {id}.")));
let node: &Node = graph.nodes.get(&id).unwrap_or(&empty_node); let node: &Node = graph.nodes.get(&id).unwrap_or(&empty_node);
let mut context = tera::Context::new();
context.insert("id", &id); context.insert("id", &id);
context.insert("title", &node.title); context.insert("title", &node.title);
context.insert("connections", &node.connections.clone()); context.insert("connections", &node.connections.clone());
context.insert("incoming", &graph.incoming.get(&id)); context.insert("incoming", &graph.incoming.get(&id));
context.insert("config", &graph.meta.config);
let out_text = parser::read::<Paragraph>(&node.text); let out_text = parser::read::<Paragraph>(&node.text);
context.insert("text", &out_text); context.insert("text", &out_text);
let not_found = node.clone() == empty_node; let not_found = *node == empty_node;
let template_name = "node.html".to_string(); let template_name = "node.html".to_string();
handlers::template::by_filename( handlers::template::by_filename(

View file

@ -5,10 +5,16 @@ use axum::{
Form, Form,
}; };
use crate::{formats::populate_graph, types::Node, handlers}; use crate::{
formats::populate_graph,
handlers,
syntax::content::parser,
types::{Config, Node},
syntax::content::elements::{span::Span, paragraph::Paragraph},
};
#[expect(clippy::unused_async)] #[expect(clippy::unused_async)]
pub async fn nexus(template: &str) -> Response<Body> { pub async fn page(template: &str) -> Response<Body> {
let mut context = tera::Context::new(); let mut context = tera::Context::new();
let graph = populate_graph(); let graph = populate_graph();
let root_node = graph.get_root().unwrap_or_default(); let root_node = graph.get_root().unwrap_or_default();
@ -17,6 +23,14 @@ pub async fn nexus(template: &str) -> Response<Body> {
context.insert("nodes", &nodes); context.insert("nodes", &nodes);
context.insert("root_node", &root_node); context.insert("root_node", &root_node);
let text_parsed_config = Config {
footer_text: parser::read::<Span>(&graph.meta.config.footer_text),
about_text: parser::read::<Paragraph>(&graph.meta.config.about_text),
..graph.meta.config
};
context.insert("config", &text_parsed_config);
handlers::template::by_filename(template, &context, 500, None, false) handlers::template::by_filename(template, &context, 500, None, false)
} }

View file

@ -1,6 +1,6 @@
use std::{backtrace, io, panic}; use std::{backtrace, io, panic};
use en::{ONSET, syntax, dev}; use en::{ONSET, dev, formats::populate_graph, syntax};
#[tokio::main] #[tokio::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
@ -25,7 +25,8 @@ async fn main() -> io::Result<()> {
} }
})); }));
let app = en::router::new(); let graph = populate_graph();
let app = en::router::new(&graph);
let listener = let listener =
tokio::net::TcpListener::bind(&address).await.map_err(|e| { tokio::net::TcpListener::bind(&address).await.map_err(|e| {

View file

@ -1,22 +1,14 @@
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use crate::{handlers, formats::Format}; use crate::{formats::Format, handlers, types::Graph};
pub fn new() -> Router { pub fn new(graph: &Graph) -> Router {
Router::new() let mut router = Router::new()
.route( .route(
"/", "/",
get(|| handlers::navigation::nexus("index.html")) get(|| handlers::navigation::page("index.html"))
.post(handlers::navigation::search), .post(handlers::navigation::search),
) )
.route(
"/graph/toml",
get(|| handlers::fixed::serial(&Format::Toml)),
)
.route(
"/graph/json",
get(|| handlers::fixed::serial(&Format::Json)),
)
.route( .route(
"/static/style.css", "/static/style.css",
get(|| handlers::fixed::file("./static/style.css", "text/css")), get(|| handlers::fixed::file("./static/style.css", "text/css")),
@ -31,11 +23,32 @@ pub fn new() -> Router {
"/node/{node_id}", "/node/{node_id}",
get(handlers::graph::node).post(handlers::graph::node), get(handlers::graph::node).post(handlers::graph::node),
) )
.route("/tree", get(|| handlers::navigation::nexus("tree.html"))) .fallback(handlers::error::not_found);
.route("/about", get(|| handlers::template::fixed("about.html")))
.route( if graph.meta.config.about {
"/acknowledgments", router = router
get(|| handlers::template::fixed("acknowledgments.html")), .route("/about", get(|| handlers::navigation::page("about.html")));
) }
.fallback(handlers::error::not_found)
if graph.meta.config.tree {
router = router
.route("/tree", get(|| handlers::navigation::page("tree.html")));
}
if graph.meta.config.raw {
if graph.meta.config.raw_json {
router = router.route(
"/graph/json",
get(|| handlers::fixed::serial(&Format::Json)),
);
}
if graph.meta.config.raw_toml {
router = router.route(
"/graph/toml",
get(|| handlers::fixed::serial(&Format::Toml)),
);
}
}
router
} }

View file

@ -26,7 +26,7 @@ impl Display for Level {
} }
} }
pub(in crate::syntax::content) struct Header { pub struct Header {
level: Level, level: Level,
text: String, text: String,
} }
@ -70,6 +70,7 @@ impl Parseable for Header {
format!("<h{}>{}</h{0}>", &self.level, self.text) format!("<h{}>{}</h{0}>", &self.level, self.text)
} }
} }
impl Display for Header { impl Display for Header {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Level {} Header: <{}>", &self.level, self.text) write!(f, "Level {} Header: <{}>", &self.level, self.text)

View file

@ -0,0 +1,28 @@
use std::fmt::Display;
use crate::syntax::content::{Parseable, Lexeme};
pub struct Span {
text: String,
}
impl Parseable for Span {
fn probe(lexeme: &Lexeme) -> bool {
!lexeme.raw.trim().is_empty()
}
fn lex(lexeme: &Lexeme) -> Self {
Self {
text: lexeme.raw.trim().to_owned(),
}
}
fn render(&self) -> String {
format!("<span>{}</span>", &self.text)
}
}
impl Display for Span {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Span: <{}>", &self.text)
}
}

View file

@ -6,10 +6,10 @@ use serde::{Serialize, Deserialize};
pub struct Graph { pub struct Graph {
pub nodes: HashMap<String, Node>, pub nodes: HashMap<String, Node>,
pub root_node: String, pub root_node: String,
#[serde(default)]
pub messages: Vec<String>,
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
pub incoming: HashMap<String, Vec<Edge>>, pub incoming: HashMap<String, Vec<Edge>>,
#[serde(default)]
pub meta: Meta,
} }
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)] #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
@ -37,16 +37,87 @@ pub struct Edge {
pub detached: bool, pub detached: bool,
} }
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub struct Meta {
pub config: Config,
#[serde(default = "mkversion")]
pub version: (u8, u8, u8),
#[serde(default)]
pub messages: Vec<String>,
}
// See: https://github.com/serde-rs/serde/issues/368
fn mkversion() -> (u8, u8, u8) {
(0, 0, 0)
}
#[expect(clippy::struct_excessive_bools)]
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub struct Config {
#[serde(default)]
pub site_title: String,
#[serde(default)]
pub site_description: String,
#[serde(default = "mktrue")]
pub footer: bool,
#[serde(default = "mktrue")]
pub footer_credits: bool,
#[serde(default = "mktrue")]
pub footer_date: bool,
#[serde(default)]
pub footer_text: String,
#[serde(default = "mktrue")]
pub about: bool,
#[serde(default)]
pub about_text: String,
#[serde(default = "mktrue")]
pub tree: bool,
#[serde(default = "mktrue")]
pub raw: bool,
#[serde(default = "mktrue")]
pub raw_toml: bool,
#[serde(default = "mktrue")]
pub raw_json: bool,
#[serde(default = "mktrue")]
pub index_search: bool,
#[serde(default = "mktrue")]
pub index_node_list: bool,
#[serde(default = "mktrue")]
pub tree_node_text: bool,
}
// See: https://github.com/serde-rs/serde/issues/368
fn mktrue() -> bool {
true
}
impl Graph { impl Graph {
pub fn new(message: Option<String>) -> Graph { pub fn new(message: Option<String>) -> Graph {
Self { Self {
nodes: HashMap::new(), nodes: HashMap::new(),
root_node: "VoidNode".to_string(), root_node: "VoidNode".to_string(),
incoming: HashMap::new(), incoming: HashMap::new(),
messages: vec![ meta: Meta {
message config: Config {
.unwrap_or("This graph is empty or in error".to_string()), site_title: String::new(),
], site_description: String::new(),
footer: true,
footer_credits: true,
footer_date: true,
footer_text: String::new(),
about: true,
about_text: String::new(),
tree: true,
raw: true,
raw_toml: true,
raw_json: true,
index_search: true,
index_node_list: true,
tree_node_text: true,
},
version: (0, 1, 0),
messages: message.map_or(vec![], |m| vec![m]),
},
} }
} }

View file

@ -122,3 +122,23 @@ To learn more about TOML, you can visit its website at <toml.io>.
To see the TOML declaration that translates into the rendered graph you are reading right now, visit the "TOML Graph" link on the top navigation bar. To see the TOML declaration that translates into the rendered graph you are reading right now, visit the "TOML Graph" link on the top navigation bar.
""" """
[nodes.Acknowledgments]
text = """
en is only possible thanks to a number of projects and people:
- [The Rust Programing Language](https://rust-lang.org/)
- [Tokio](https://tokio.rs/)
- [Axum](https://github.com/tokio-rs/axum)
- [Tera](https://keats.github.io/tera/)
- [Serde](https://serde.rs/) and the [toml crate](https://github.com/toml-rs/toml)
- [Bacon](https://dystroy.org/bacon/config/)
"""
[meta.config]
footer_credits = false
footer_text = """
made by jutty acknowledgements source code
"""

View file

@ -5,6 +5,9 @@
{%- block body %} {%- block body %}
<h1>About</h1> <h1>About</h1>
{% if config.about_text %}
{{ config.about_text | safe }}
{% else %}
<p>en is a program to create a connected collection of texts.</p> <p>en is a program to create a connected collection of texts.</p>
<p>You define your graph using a plain-text configuration file, en reads this file and generates a website like the one you are browsing right now.</p> <p>You define your graph using a plain-text configuration file, en reads this file and generates a website like the one you are browsing right now.</p>
@ -17,5 +20,6 @@
<li><a href="https://en.jutty.dev/node/Documentation">Documentation</a></li> <li><a href="https://en.jutty.dev/node/Documentation">Documentation</a></li>
<li><a href="https://codeberg.org/jutty/en">Source code repository</a></li> <li><a href="https://codeberg.org/jutty/en">Source code repository</a></li>
</ul> </ul>
{% endif %}
{%- endblock body %} {%- endblock body %}

View file

@ -1,20 +0,0 @@
{% extends "base.html" %}
{% block title %}Acknowledgments{% endblock title %}
{%- block body %}
<h1>Acknowledgments</h1>
<p>en is only possible thanks to a number of projects and people:</p>
<ul>
<li><a href="https://rust-lang.org/">The Rust Programing Language</a></li>
<li><a href="https://tokio.rs/">Tokio</a> and
<a href="https://github.com/tokio-rs/axum">Axum</a></li>
<li><a href="https://keats.github.io/tera/">Tera</a></li>
<li><a href="https://serde.rs/">Serde</a> and the
<a href="https://github.com/toml-rs/toml">toml crate</a></li>
<li><a href="https://dystroy.org/bacon/config/">Bacon</a></li>
</ul>
{%- endblock body %}

View file

@ -1,7 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
{% if config.site_title %}
<title>{% block title %}{% endblock title %} &bullet; {{config.site_title}}</title>
{% else %}
<title>{% block title %}{% endblock title %} &bullet; en</title> <title>{% block title %}{% endblock title %} &bullet; en</title>
{% endif %}
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" > <meta http-equiv="Content-Type" content="text/html; charset=utf-8" >
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/style.css" rel="stylesheet"> <link href="/static/style.css" rel="stylesheet">
@ -13,10 +17,20 @@
<nav style="text-align: center;"> <nav style="text-align: center;">
<ul style="display: inline; padding-left: 0;"> <ul style="display: inline; padding-left: 0;">
<li style="display: inline;"><a href="/">Index</a></li> <li style="display: inline;"><a href="/">Index</a></li>
{% if config.about %}
<li style="display: inline;"><a href="/about">About</a></li> <li style="display: inline;"><a href="/about">About</a></li>
{% endif %}
{% if config.tree %}
<li style="display: inline;"><a href="/tree">Tree</a></li> <li style="display: inline;"><a href="/tree">Tree</a></li>
{% endif %}
{% if config.raw %}
{% if config.raw_toml %}
<li style="display: inline;"><a href="/graph/toml">TOML Graph</a></li> <li style="display: inline;"><a href="/graph/toml">TOML Graph</a></li>
{% endif %}
{% if config.raw_json %}
<li style="display: inline;"><a href="/graph/json">JSON Graph</a></li> <li style="display: inline;"><a href="/graph/json">JSON Graph</a></li>
{% endif %}
{% endif %}
</ul> </ul>
<hr> <hr>
</nav> </nav>
@ -24,22 +38,30 @@
{% block body %} {% block body %}
{% endblock body %} {% endblock body %}
</main> </main>
{% if config.footer %}
<footer> <footer>
<hr> <hr>
<div> <div>
<cite>made by <a href="https://jutty.dev">jutty</a></cite> {% if config.footer_text %}{{ config.footer_text | safe }}{% endif %}
&bullet; {% if config.footer_text and (config.footer_credits or config.footer_date) %}
<a href="/acknowledgments">acknowledgments</a>
&bullet;
<a href="https://codeberg.org/jutty/en">source code</a>
<br/> <br/>
{% endif %}
{% if config.footer_credits %}
<cite>made with <a href="https://en.jutty.dev">en</a></cite>, a non-linear writing instrument
{% endif %}
{% if config.footer_credits and config.footer_date %}
<br/>
{% endif %}
{% if config.footer_date %}
built built
<time> <time>
{{ now(utc=true) | date(format="%Y-%m-%d %H:%M") }} UTC {{ now(utc=true) | date(format="%Y-%m-%d %H:%M") }} UTC
</time> </time>
&bullet; &bullet;
<time>{{ now(timestamp=true) }} Unix Epoch</time> <time>{{ now(timestamp=true) }} Unix Epoch</time>
{% endif %}
</div> </div>
</footer> </footer>
{% endif %}
</body> </body>
</html> </html>

View file

@ -1,2 +1,14 @@
<p>There are no nodes. The graph is either empty or failed to parse.</p> <p>There are no nodes. The graph is either empty or failed to parse.</p>
<p>Check the <a href="/graph/toml">raw endpoints</a> for possible parsing errors.</p> {% if config.raw %}
<p>Check the
{% if config.raw_toml %}
<a href="/graph/toml">
{% elif config.raw_json %}
<a href="/graph/json">
{% endif %}
raw endpoints
{% if config.raw_toml or config.raw_json %}
</a>
{% endif %}
for possible parsing errors.</p>
{% endif %}

View file

@ -11,7 +11,7 @@
fallen<br/> fallen<br/>
out of the circle<br/> out of the circle<br/>
you are welcome to climb<br/> you are welcome to climb<br/>
back onto the <a href="/tree">tree</a> back onto the {% if config.tree %}<a href="/tree">tree</a>{% else %}tree{% endif %}
</em> </em>
</div> </div>
</p> </p>

View file

@ -3,15 +3,24 @@
{% block title %}Index{% endblock title %} {% block title %}Index{% endblock title %}
{%- block body %} {%- block body %}
<h1>en</h1> <h1>{%if config.site_title %}{{ config.site_title }}{% else %}en{%endif%}</h1>
<p> <p>
<em>A non-linear writing instrument.</em> <em>
{% if config.site_description %}
{{config.site_description}}
{% else %}
A non-linear writing instrument.
{% endif %}
</em>
{% if nodes %} {% if nodes %}
{% if config.index_search %}
<form method="post"> <form method="post">
<label for="node">Find by ID:</label> <label for="node">Find by ID:</label>
<input type="text" name="node" required/> <input type="text" name="node" required/>
<input type="submit" value="Submit"/> <input type="submit" value="Submit"/>
</form> </form>
{% endif %}
{% if config.index_node_list %}
<hr> <hr>
<h2>Nodes</h2> <h2>Nodes</h2>
<nav> <nav>
@ -31,6 +40,7 @@
{% endif %} {% endif %}
</ul> </ul>
</nav> </nav>
{% endif %}
{% else %} {% else %}
<hr> <hr>
{% include "empty.html" %} {% include "empty.html" %}

View file

@ -2,6 +2,9 @@
{% block title %}Tree{% endblock title %} {% block title %}Tree{% endblock title %}
<!-- TODO Tera has macros, which could considerably simplify this template -->
<!-- See: https://keats.github.io/tera/docs/#macros -->
{%- block body %} {%- block body %}
{% if nodes or root_node %} {% if nodes or root_node %}
<h1>Tree</h1> <h1>Tree</h1>
@ -12,7 +15,9 @@
<ul> <ul>
<li> <li>
<a href="/node/{{root_node.id}}">{{root_node.title}}</a> <a href="/node/{{root_node.id}}">{{root_node.title}}</a>
{% if root_node.connections or config.tree_node_text %}
<ul> <ul>
{% if config.tree_node_text %}
<li><strong>Text:</strong> <li><strong>Text:</strong>
<ul style="display: inline; padding-left: 0;"> <ul style="display: inline; padding-left: 0;">
<li style="display: inline;"> <li style="display: inline;">
@ -24,16 +29,18 @@
</details> </details>
</li> </li>
</ul> </ul>
{% if root_node.connections %} {% endif %}
<li><strong>Connections</strong> {% if root_node.connections %}
<ul> {% if config.tree_node_text %}<li><strong>Connections</strong>
{% for connection in root_node.connections %} <ul>{% endif %}
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li> {% for connection in root_node.connections %}
{% endfor %} <li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
</ul> {% endfor %}
</li> {% if config.tree_node_text %}</ul>
{% endif %} </li>{% endif %}
{% endif %}
</ul> </ul>
{% endif %}
</li> </li>
</ul> </ul>
{% endif %} {% endif %}
@ -44,7 +51,9 @@
{% for node in nodes %} {% for node in nodes %}
<li> <li>
<a href="/node/{{node.id}}">{{node.title}}</a> <a href="/node/{{node.id}}">{{node.title}}</a>
{% if node.connections or config.tree_node_text %}
<ul> <ul>
{% if config.tree_node_text %}
<li><strong>Text:</strong> <li><strong>Text:</strong>
<ul style="display: inline; padding-left: 0;"> <ul style="display: inline; padding-left: 0;">
<li style="display: inline;"> <li style="display: inline;">
@ -56,18 +65,20 @@
</details> </details>
</li> </li>
</ul> </ul>
{% if node.connections %}
<li><strong>Connections</strong>
<ul>
{% for connection in node.connections %}
{% if not connection.detached %}
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
{% endif %} {% endif %}
{% endfor %} {% if node.connections %}
</ul> {% if config.tree_node_text %}<li><strong>Connections</strong>
</li> <ul>{% endif %}
{% endif %} {% for connection in node.connections %}
{% if not connection.detached %}
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
{% endif %}
{% endfor %}
{% if config.tree_node_text %}</ul>
</li>{% endif %}
{% endif %}
</ul> </ul>
{% endif %}
</li> </li>
{% endfor %} {% endfor %}
{% endif %} {% endif %}