Simplify handler contexts, template and style tweaks

This commit is contained in:
Juno Takano 2026-01-14 02:52:48 -03:00
commit 940aadb6e5
14 changed files with 176 additions and 110 deletions

View file

@ -43,8 +43,7 @@ pub fn new(graph: &Graph) -> Router {
}
if graph.meta.config.tree {
router = router
.route("/tree", get(|| handlers::navigation::page("tree.html")));
router = router.route("/tree", get(handlers::navigation::tree));
}
if graph.meta.config.raw {

View file

@ -24,9 +24,9 @@ pub(in crate::router::handlers) fn by_code(
fn make_body(code: Option<u16>, message: Option<&str>) -> String {
let mut context = tera::Context::default();
let graph = Graph::load();
let out_code = code.unwrap_or(500);
let out_message = &message.unwrap_or("Unknown error");
let config = Graph::load().meta.config;
context.insert(
"title",
@ -35,9 +35,9 @@ fn make_body(code: Option<u16>, message: Option<&str>) -> String {
.to_string(),
);
context.insert("graph", &graph);
context.insert("message", out_message);
context.insert("status_code", &out_code.to_string());
context.insert("config", &config);
handlers::template::render(
"error.html",

View file

@ -1,15 +1,12 @@
use axum::response::IntoResponse as _;
use axum::{body::Body, extract::Path, http::Response, response::Redirect};
use crate::graph::Edge;
use crate::{graph::Graph, router::handlers, graph::Node};
pub async fn node(Path(id): Path<String>) -> Response<Body> {
let graph = Graph::load();
let result = graph.find_node(&id);
let found = result.node.is_some();
let nodes: Vec<Node> = graph.nodes.clone().into_values().collect();
let not_found = result.node.is_none();
let node = result
.node
.unwrap_or(Node::new(Some(format!("Could not find node ID {id}."))));
@ -27,24 +24,14 @@ pub async fn node(Path(id): Path<String>) -> Response<Body> {
}
let mut context = tera::Context::default();
context.insert("graph", &graph);
context.insert("node", &node);
context.insert("nodes", &nodes);
context.insert(
"connections",
&node
.connections
.clone()
.unwrap_or_default()
.values()
.collect::<Vec<&Edge>>(),
);
context.insert("incoming", &graph.incoming.get(&id));
context.insert("config", &graph.meta.config);
handlers::template::by_filename(
"node.html",
&context,
if not_found { 404 } else { 500 },
if found { 500 } else { 404 },
Some(
format!(
"Failed to generate page for node {} (ID {}).\n\
@ -53,7 +40,7 @@ pub async fn node(Path(id): Path<String>) -> Response<Body> {
)
.to_owned(),
),
not_found,
!found,
)
}

View file

@ -20,6 +20,28 @@ pub async fn page(template: &str) -> Response<Body> {
handlers::template::by_filename(template, &context, 500, None, false)
}
pub async fn tree() -> Response<Body> {
let mut context = tera::Context::default();
let mut graph = Graph::load();
context.insert("graph", &graph);
if let Some(root_node) = graph.get_root() {
graph.nodes.remove(&root_node.id);
context.insert("root_node", &root_node);
context.insert(
"nodes",
&graph.nodes.values().cloned().collect::<Vec<Node>>(),
);
} else {
context.insert(
"nodes",
&graph.nodes.values().cloned().collect::<Vec<Node>>(),
);
}
handlers::template::by_filename("tree.html", &context, 500, None, false)
}
pub async fn data() -> Response<Body> {
let mut context = tera::Context::default();
let graph = Graph::load();

View file

@ -157,10 +157,8 @@ mod tests {
let node = crate::graph::Node::new(Some(payload.to_string()));
let graph = Graph::load();
context.insert("node", &node);
context
.insert("text", &crate::syntax::content::parse(&node.text, &graph));
context.insert("graph", &graph);
context.insert("incoming", &graph.incoming.get(&node.id));
context.insert("config", &graph.meta.config);
let (body, status) = render("node.html", &context, None);
assert_eq!(status, 200);
assert!(body.matches(payload).count() == 1);

View file

@ -10,12 +10,17 @@ body {
grid-template-rows: auto 1fr auto;
}
main {
width: 90vw;
margin: 0 2.5vw;
}
pre {
max-width: 96vw;
max-width: 95%;
overflow: auto;
box-sizing: border-box;
padding: 10px;
margin: 20px 2vw;
margin: 20px 2.5%;
}
@ -39,7 +44,7 @@ a {
a.detached {
color: #595959;
text-decoration-color: none;
text-decoration-color: #444444;
}
a.external {
@ -75,6 +80,15 @@ span.label {
font-size: 0.7em;
}
span.root-label {
color: #fff;
font-weight: bolder;
background-color: #106363;
border: outset 1px #ffffff;
box-shadow: 2px 2px #00ffff;
}
span.id-label {
background-color: #e0e0e0;
border: solid 1px #d0d0d0;
@ -86,10 +100,18 @@ span.hidden-label {
border: solid 1px #d0d0d0;
}
h1, h2, h3 {
font-weight: lighter;
margin: 20px 0 12px;
}
h1.node-title {
display: inline;
margin: 10px 0;
font-weight: 300;
}
h3 {
font-size: 20px;
}
footer div {
@ -106,8 +128,17 @@ span.detached-connection {
filter: opacity(60%);
}
div.connections-container {
margin-bottom: 20px;
}
hr.connections {
margin-top: 60px;
}
nav#nav-main {
text-align: center;
margin-bottom: 20px;
}
#nav-main-spread {
@ -174,6 +205,14 @@ td, th {
border: 0.5px dotted #666;
}
summary {
margin-bottom: 15px;
}
hr {
border: 1px dashed #555;
}
@media (prefers-color-scheme: dark) {
* {
background-color: #222222;
@ -226,3 +265,12 @@ td, th {
margin: 0;
}
}
@media (min-width: 601px) {
div.connections-container {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
}
}

View file

@ -5,8 +5,8 @@
{%- block body %}
<h1>About</h1>
{% if config.about_text %}
{{ config.about_text | safe }}
{% if graph.meta.config.about_text %}
{{ graph.meta.config.about_text | safe }}
{% else %}
<p>en is a program to create a connected collection of texts.</p>

View file

@ -1,12 +1,12 @@
<!DOCTYPE html>
{% if config.content_language %}
<html lang="{{ config.content_language }}">
{% if graph.meta.config.content_language %}
<html lang="{{ graph.meta.config.content_language }}">
{% else %}
<html>
{% endif %}
<head>
{% if config.site_title %}
<title>{% block title %}{% endblock title %} &bullet; {{config.site_title}}</title>
{% if graph.meta.config.site_title %}
<title>{% block title %}{% endblock title %} &bullet; {{graph.meta.config.site_title}}</title>
{% else %}
<title>{% block title %}{% endblock title %} &bullet; en</title>
{% endif %}
@ -19,33 +19,36 @@
</head>
<body>
<nav id="nav-main">
<div {% if config.node_selector or config.navbar_search %}id="nav-main-spread"{% endif %}>
<div {% if graph.meta.config.node_selector or graph.meta.config.navbar_search %}id="nav-main-spread"{% endif %}>
<ul id="nav-anchors">
<li><a href="/">Index</a></li>
{% if config.about %}
{% if graph.meta.config.about %}
<li><a href="/about">About</a></li>
{% endif %}
{% if config.tree %}
{% if graph.meta.config.tree %}
<li><a href="/tree">Tree</a></li>
{% endif %}
{% if config.raw %}
{% if graph.meta.config.raw %}
<li><a href="/data">Data</a></li>
{% endif %}
</ul>
{% if config.node_selector or config.navbar_search %}
{% if graph.meta.config.node_selector or graph.meta.config.navbar_search %}
<div class="nav-inputs">
{% if nodes and config.node_selector %}
{% if graph.nodes and graph.meta.config.node_selector %}
<form class="node-selector" action="/redirect">
<select class="node-selector" name="node">
<option value="">Nodes</option>
{% for node in nodes | filter(attribute="hidden", value=false) %}
{% for _, node in graph.nodes %}
{% if node.hidden or node.redirect %}{% continue %}{% endif %}
{% if not node.hidden and not node.redirect %}
<option value="{{node.id}}">{{node.title}}</option>
{% endif %}
{% endfor %}
</select>
<button type="submit">Go</button>
</form>
{% endif %}
{% if config.navbar_search %}
{% if graph.meta.config.navbar_search %}
<form class="navbar-search" action="/search">
<input type="text" name="node" required/>
<input type="submit" value="Find"/>
@ -60,20 +63,20 @@
{% block body %}
{% endblock body %}
</main>
{% if config.footer %}
{% if graph.meta.config.footer %}
<footer>
<hr>
<div>
{% if config.footer_text %}{{ config.footer_text | safe }}{% endif %}
{% if config.footer_text and (config.footer_credits or config.footer_date) %}
{% if graph.meta.config.footer_text %}{{ graph.meta.config.footer_text | safe }}{% endif %}
{% if graph.meta.config.footer_text and (graph.meta.config.footer_credits or graph.meta.config.footer_date) %}
{% endif %}
{% if config.footer_credits %}
{% if graph.meta.config.footer_credits %}
<cite>made with <a href="https://en.jutty.dev">en</a>, a non-linear writing instrument</cite>
{% endif %}
{% if config.footer_credits and config.footer_date %}
{% if graph.meta.config.footer_credits and graph.meta.config.footer_date %}
<br/>
{% endif %}
{% if config.footer_date %}
{% if graph.meta.config.footer_date %}
built
<time>
{{ now(utc=true) | date(format="%Y-%m-%d %H:%M") }} UTC

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% set config = graph.meta.config %}
{% block title %}Data{% endblock title %}
@ -10,30 +10,36 @@
<table>
<tr>
<td>Detached edges</td> <td>{{ detached_count }}</td>
<td>
<a href="#detached-edges">Detached edges</a>
</td>
<td>
<a href="#detached-edges:~:text=Anchors">{{ detached_count }}</a>
</td>
</tr>
</table>
<h3>Detached edges</h3>
<h3 id="detached-edges">Detached edges</h3>
<details>
<summary>Expand to list all detached edges.</summary>
<ul>
<summary>Expand to see all detached edges.</summary>
<table>
<th>Destination</th><th>Anchors</th>
{% for pair in detached_pairs %}
<li>{{ pair.0 }}: {{ pair.1 }}</li>
<tr><td>{{ pair.0 }}</td><td>{{ pair.1 }}</td></tr>
{% endfor %}
</ul>
</table>
</details>
{% if graph.meta.config.raw_toml or graph.meta.config.raw_json %}
{% if config.raw_toml or config.raw_json %}
<h2>Raw formats</h2>
<p>The raw data used to render this graph is available in the following formats:</p>
<ul>
{% if graph.meta.config.raw_toml %}
{% if config.raw_toml %}
<li><a href="/graph/toml">TOML</a></li>
{% endif %}
{% if graph.meta.config.raw_json %}
{% if config.raw_json %}
<li><a href="/graph/json">JSON</a></li>
{% endif %}
</ul>

View file

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

View file

@ -4,15 +4,15 @@
<h1>
{{ title | default(value="Unknown error") }}
</h1>
{{ message | default(value="Unexpected error") | linebreaksbr | safe }}
{% if config.error_poem %}
{{ message | default(value="No message provided.") | linebreaksbr | safe }}
{% if graph.meta.config.error_poem %}
<hr>
<div id="error-poem">
<em>
fallen<br/>
out of the circle<br/>
you are welcome to climb<br/>
back onto the {% if config.tree %}<a href="/tree">tree</a>{% else %}tree{% endif %}
back onto the {% if graph.meta.config.tree %}<a href="/tree">tree</a>{% else %}tree{% endif %}
</em>
</div>
{% endif %}

View file

@ -3,47 +3,50 @@
{% block title %}Index{% endblock title %}
{%- block body %}
<h1>{%if config.site_title %}{{ config.site_title }}{% else %}en{%endif%}</h1>
<h1>{%if graph.meta.config.site_title %}{{ graph.meta.config.site_title }}{% else %}en{%endif%}</h1>
<p>
<i>
{% if config.site_description %}
{{config.site_description}}
{% if graph.meta.config.site_description %}
{{graph.meta.config.site_description}}
{% else %}
A non-linear writing instrument.
{% endif %}
</i>
{% if nodes %}
{% if config.navbar_search %}
{% if graph.nodes %}
{% if graph.meta.config.navbar_search %}
<form method="post">
<input type="text" name="node" required/>
<input type="submit" value="Find"/>
</form>
{% endif %}
{% if config.index_node_list or config.index_root_node %}
{% if graph.meta.config.index_node_list or graph.meta.config.index_root_node %}
<hr>
{% if config.index_node_list %}
{% if graph.meta.config.index_node_list %}
<h2>Nodes</h2>
{% endif %}
<nav>
{% if root_node and config.index_root_node %}
{% if graph.root_node and graph.meta.config.index_root_node %}
<p>
<strong>Root</strong>:
<a href="/node/{{root_node.id}}">{{root_node.title}}</a>
<a href="/node/{{graph.root_node}}">{{graph.root_node}}</a>
</p>
{% endif %}
{% if nodes and config.index_node_list %}
{% if graph.nodes and graph.meta.config.index_node_list %}
<ul>
{% for node in nodes | slice(end=config.index_node_count) %}
{% if node.id != root_node.id and not node.hidden %}
{% for _, node in graph.nodes %}
{% if loop.index > graph.meta.config.index_node_count %}
{% break %}
{% endif %}
{% if node.id != graph.root_node and not node.hidden and not node.redirect %}
<li><a href="/node/{{node.id}}">{{node.title}}</a></li>
{% endif %}
{% endfor %}
</ul>
{% if config.index_node_count < nodes | length %}
{% if graph.meta.config.index_node_count < graph.nodes | length %}
<br/>
<em id="index-node-count">
Listing {{ config.index_node_count }} of {{ nodes | length }} nodes.
{% if config.tree %}
Listing {{ graph.meta.config.index_node_count }} of {{ graph.nodes | length }} nodes.
{% if graph.meta.config.tree %}
<br/>
See the <a href="/tree">tree</a> for a full list.
{% endif %}

View file

@ -9,46 +9,52 @@
<div class="labels">
{% if node.title != node.id %}<span class="label id-label">ID: {{ node.id }}</span>{% endif %}
{% if node.hidden %}<span class="label hidden-label">Hidden</span>{% endif %}
{% if node.id == graph.root_node %}<span class="label root-label">Root</span>{% endif %}
</div>
</div>
{{ node.text | safe }}
</section>
{% if connections or incoming %}
<aside>
<hr>
{% if node.connections or incoming %}
<aside class="connections">
<hr class="connections">
<h2>Connections</h2>
{% if connections %}
<div class="connections-container">
{% if node.connections %}
<div class="connections-outgoing">
<h3>Outgoing</h3>
<ul>
{% for connection in connections | filter(attribute="detached", value=false) %}
{% for _, connection in node.connections %}
{% if connection.detached %}
{% continue %}
{% endif %}
<li>
<strong>{{node.id}}</strong>
&raquo;
<a href="/node/{{connection.to}}">{{connection.to}}</a>
</li>
{% endfor %}
{% for connection in connections | filter(attribute="detached", value=true) %}
{% for _, connection in node.connections %}
{% if not connection.detached %}
{% continue %}
{% endif %}
<li>
<strong>{{node.id}}</strong>
&raquo;
<span class="detached-connection">{{connection.to}}</span>
</li>
{% endfor %}
</ul>
{% else %}
<em>No outgoing connections.</em>
</div>
{% endif %}
{% if incoming %}
<h3>Incoming connections</h3>
<div class="connections-incoming">
<h3>Incoming</h4>
<ul>
{% for connection in incoming %}
<li>
<strong>{{connection.to}}</strong>
&laquo;
<a href="/node/{{connection.from}}">{{connection.from}}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</aside>
{% endif %}
{%- endblock body %}

View file

@ -1,32 +1,31 @@
{% extends "base.html" %}
{% block title %}Tree{% endblock title %}
{% set config = graph.meta.config %}
<!-- TODO Tera has macros, which could considerably simplify this template -->
<!-- See: https://keats.github.io/tera/docs/#macros -->
{% block title %}Tree{% endblock title %}
{%- block body %}
{% if nodes or root_node %}
<h1>Tree</h1>
<p><strong>Total nodes:</strong> {{ nodes | length }}</p>
{% if root_node %}
<h2>Root node</h2>
<ul>
{% if root_node and not root_node.hidden %}
<li>
<a href="/node/{{root_node.id}}">{{root_node.title}}</a>
<span class="label root-label">Root</span>
{% if root_node.connections or config.tree_node_summary %}
<ul>
{% if config.tree_node_summary %}
<li class="tree-node-summary">
{{ root_node.summary }}
{{root_node.summary}}
</li>
{% endif %}
{% if root_node.connections %}
{% if config.tree_node_summary %}<li><strong>Connections</strong>
<ul>{% endif %}
{% for _, connection in root_node.connections %}
{% if not connection.detached %}
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
{% endif %}
{% endfor %}
{% if config.tree_node_summary %}</ul>
</li>{% endif %}
@ -34,13 +33,9 @@
</ul>
{% endif %}
</li>
</ul>
{% endif %}
{% if nodes %}
<h2>All nodes</h2>
<ul>
{% for node in nodes | filter(attribute="hidden", value=false)%}
{% for node in nodes %}
{% if node.hidden %}{% continue %}{% endif %}
<li>
<a href="/node/{{node.id}}">{{node.title}}</a>
{% if node.connections or config.tree_node_summary %}
@ -65,7 +60,6 @@
{% endif %}
</li>
{% endfor %}
{% endif %}
</ul>
{% else %}