Make edge modulation steps more consistent
This commit is contained in:
parent
db8c02df04
commit
bd5d46a5d4
8 changed files with 227 additions and 118 deletions
58
src/graph.rs
58
src/graph.rs
|
|
@ -3,6 +3,7 @@ use std::collections::HashMap;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use crate::syntax::content;
|
use crate::syntax::content;
|
||||||
|
use crate::prelude::*;
|
||||||
pub use {
|
pub use {
|
||||||
node::Node,
|
node::Node,
|
||||||
edge::Edge,
|
edge::Edge,
|
||||||
|
|
@ -25,10 +26,23 @@ pub struct Graph {
|
||||||
pub meta: Meta,
|
pub meta: Meta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Default, Debug)]
|
||||||
pub struct QueryResult {
|
pub struct QueryResult {
|
||||||
pub node: Option<Node>,
|
pub node: Option<Node>,
|
||||||
pub redirect: bool,
|
pub redirect: bool,
|
||||||
|
pub exact: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for QueryResult {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
let meta = if self.redirect { "[redirect] " } else { "" };
|
||||||
|
let node = if let Some(n) = &self.node {
|
||||||
|
n.id.clone()
|
||||||
|
} else {
|
||||||
|
String::from("No Match")
|
||||||
|
};
|
||||||
|
write!(f, "QueryResult: {meta}{node}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Graph {
|
impl Graph {
|
||||||
|
|
@ -46,36 +60,62 @@ impl Graph {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map_lowercase_keys(&mut self) {
|
||||||
|
for key in self.nodes.keys() {
|
||||||
|
self.lowercase_keymap
|
||||||
|
.insert(key.clone().to_lowercase(), key.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_node(&self, query: &str) -> QueryResult {
|
pub fn find_node(&self, query: &str) -> QueryResult {
|
||||||
let collapsed_query = query.trim().replace(" ", "");
|
let collapsed_query = query.trim().replace(" ", "");
|
||||||
|
|
||||||
|
if query == collapsed_query {
|
||||||
|
log!("Chasing candidate for query {query}");
|
||||||
|
} else {
|
||||||
|
log!(
|
||||||
|
"Chasing candidate for query {query}, collapsed {collapsed_query}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let candidate = if let Some(exact_match) = self.nodes.get(query) {
|
let candidate = if let Some(exact_match) = self.nodes.get(query) {
|
||||||
|
log!("Elected exact match {exact_match}");
|
||||||
QueryResult {
|
QueryResult {
|
||||||
node: Some(exact_match.clone()),
|
node: Some(exact_match.clone()),
|
||||||
|
exact: true,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
}
|
}
|
||||||
} else if let Some(lower_key) =
|
} else if let Some(lower_key) =
|
||||||
self.lowercase_keymap.get(&collapsed_query.to_lowercase())
|
self.lowercase_keymap.get(&collapsed_query.to_lowercase())
|
||||||
{
|
{
|
||||||
|
log!("Elected non-exact match through lower key {lower_key}");
|
||||||
QueryResult {
|
QueryResult {
|
||||||
node: self.nodes.get(lower_key).cloned(),
|
node: self.nodes.get(lower_key).cloned(),
|
||||||
redirect: true,
|
exact: false,
|
||||||
}
|
|
||||||
} else {
|
|
||||||
QueryResult {
|
|
||||||
node: None,
|
|
||||||
redirect: false,
|
redirect: false,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log!("No candidate found");
|
||||||
|
QueryResult::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(candidate_node) = &candidate.node
|
if let Some(candidate_node) = &candidate.node
|
||||||
&& !candidate_node.redirect.is_empty()
|
&& !candidate_node.redirect.is_empty()
|
||||||
{
|
{
|
||||||
QueryResult {
|
log!("Recursing: candidate is a redirect");
|
||||||
node: self.find_node(&candidate_node.redirect).node,
|
if let Some(recursive_match) =
|
||||||
redirect: true,
|
self.find_node(&candidate_node.redirect).node
|
||||||
|
{
|
||||||
|
QueryResult {
|
||||||
|
node: Some(recursive_match),
|
||||||
|
exact: false,
|
||||||
|
redirect: true,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QueryResult::default()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log!("Returning candidate {candidate}");
|
||||||
candidate
|
candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ use serde::{Serialize, Deserialize};
|
||||||
pub struct Edge {
|
pub struct Edge {
|
||||||
pub to: String,
|
pub to: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub anchor: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub from: String,
|
pub from: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub detached: bool,
|
pub detached: bool,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use super::edge::Edge;
|
use super::edge::Edge;
|
||||||
|
|
@ -20,7 +22,7 @@ pub struct Node {
|
||||||
pub hidden: bool,
|
pub hidden: bool,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub connections: Option<Vec<Edge>>,
|
pub connections: Option<HashMap<String, Edge>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl Node {
|
||||||
|
|
@ -41,6 +43,40 @@ impl Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Node {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
let mut meta = String::default();
|
||||||
|
if self.title.is_empty() {
|
||||||
|
meta.push_str("title:none");
|
||||||
|
} else {
|
||||||
|
meta.push_str(&format!("title:'{}'", self.title));
|
||||||
|
}
|
||||||
|
if self.text.is_empty() {
|
||||||
|
meta.push_str(" text:none");
|
||||||
|
} else {
|
||||||
|
meta.push_str(&format!(" text:{}l", self.text.len()));
|
||||||
|
}
|
||||||
|
if self.summary.is_empty() {
|
||||||
|
meta.push_str(" summary:none");
|
||||||
|
} else {
|
||||||
|
meta.push_str(&format!(" summary:{}", self.summary.len()));
|
||||||
|
}
|
||||||
|
if self.redirect.is_empty() {
|
||||||
|
meta.push_str(" redirect:none");
|
||||||
|
} else {
|
||||||
|
meta.push_str(&format!(" redirect:{}", self.redirect));
|
||||||
|
}
|
||||||
|
let links = self.links.len();
|
||||||
|
if links > 0 {
|
||||||
|
meta.push_str(&format!(" links:{links}"));
|
||||||
|
}
|
||||||
|
if self.hidden {
|
||||||
|
meta.push_str(" hidden");
|
||||||
|
}
|
||||||
|
write!(f, "Node: ID '{}' {meta}", self.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
use axum::response::IntoResponse as _;
|
use axum::response::IntoResponse as _;
|
||||||
use axum::{body::Body, extract::Path, http::Response, response::Redirect};
|
use axum::{body::Body, extract::Path, http::Response, response::Redirect};
|
||||||
|
|
||||||
|
use crate::graph::Edge;
|
||||||
use crate::{syntax::serial::populate_graph, router::handlers, graph::Node};
|
use crate::{syntax::serial::populate_graph, router::handlers, graph::Node};
|
||||||
|
|
||||||
pub async fn node(Path(id): Path<String>) -> Response<Body> {
|
pub async fn node(Path(id): Path<String>) -> Response<Body> {
|
||||||
let graph = populate_graph();
|
let graph = populate_graph();
|
||||||
let result = graph.find_node(&id);
|
let result = graph.find_node(&id);
|
||||||
|
let found = result.node.is_some();
|
||||||
let nodes: Vec<Node> = graph.nodes.clone().into_values().collect();
|
let nodes: Vec<Node> = graph.nodes.clone().into_values().collect();
|
||||||
let not_found = result.node.is_none();
|
let not_found = result.node.is_none();
|
||||||
let node = result
|
let node = result
|
||||||
|
|
@ -19,7 +21,7 @@ pub async fn node(Path(id): Path<String>) -> Response<Body> {
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.redirect {
|
if found && !result.exact {
|
||||||
return Redirect::permanent(format!("/node/{}", node.id).as_str())
|
return Redirect::permanent(format!("/node/{}", node.id).as_str())
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +29,15 @@ pub async fn node(Path(id): Path<String>) -> Response<Body> {
|
||||||
let mut context = tera::Context::default();
|
let mut context = tera::Context::default();
|
||||||
context.insert("node", &node);
|
context.insert("node", &node);
|
||||||
context.insert("nodes", &nodes);
|
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("incoming", &graph.incoming.get(&id));
|
||||||
context.insert("config", &graph.meta.config);
|
context.insert("config", &graph.meta.config);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,18 @@ pub fn populate_graph() -> Graph {
|
||||||
Err(e) => format!("Error: {e}"),
|
Err(e) => format!("Error: {e}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let graph = deserialize_graph(&Format::TOML, &toml_source);
|
let mut graph = deserialize_graph(&Format::TOML, &toml_source);
|
||||||
modulate_graph(&graph)
|
modulate_graph(&mut graph)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modulate_graph(in_graph: &Graph) -> Graph {
|
fn modulate_graph(in_graph: &mut Graph) -> Graph {
|
||||||
|
in_graph.map_lowercase_keys();
|
||||||
let nodes = modulate_nodes(in_graph);
|
let nodes = modulate_nodes(in_graph);
|
||||||
|
|
||||||
let mut graph = Graph {
|
let mut graph = Graph {
|
||||||
incoming: make_incoming(&nodes),
|
incoming: make_incoming(&nodes),
|
||||||
lowercase_keymap: map_lowercase_keys(&nodes),
|
|
||||||
nodes,
|
nodes,
|
||||||
..in_graph.to_owned()
|
..in_graph.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
graph.parse();
|
graph.parse();
|
||||||
|
|
@ -37,65 +37,42 @@ fn modulate_graph(in_graph: &Graph) -> Graph {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modulate_nodes(graph: &Graph) -> HashMap<String, Node> {
|
fn modulate_nodes(graph: &Graph) -> HashMap<String, Node> {
|
||||||
let old_nodes = graph.nodes.clone();
|
let in_nodes = graph.nodes.clone();
|
||||||
let mut nodes: HashMap<String, Node> = HashMap::default();
|
|
||||||
|
|
||||||
for (key, node) in old_nodes.clone() {
|
let mut first_pass_nodes: HashMap<String, Node> = HashMap::default();
|
||||||
|
for (key, node) in in_nodes.clone() {
|
||||||
let connections = node.connections.clone().unwrap_or_default();
|
let connections = node.connections.clone().unwrap_or_default();
|
||||||
let mut new_edges = connections.clone();
|
let mut new_edges = connections.clone();
|
||||||
|
|
||||||
// Parse node text
|
|
||||||
let (text, tokens) = content::rich_parse(&node.text, graph);
|
|
||||||
|
|
||||||
// Modulate connections
|
// Modulate connections
|
||||||
for (i, edge) in connections.iter().enumerate() {
|
for (connection_key, edge) in connections {
|
||||||
let mut new_edge = edge.clone();
|
let mut new_edge = edge.clone();
|
||||||
|
|
||||||
// Populate empty "from" IDs in edges with node's ID
|
// Populate empty "from" IDs in edges with node's ID
|
||||||
if edge.from.is_empty() {
|
if edge.from.is_empty() {
|
||||||
new_edge.from.clone_from(&key);
|
new_edge.from.clone_from(&connection_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flag detached edges
|
// Flag detached edges
|
||||||
if !old_nodes.contains_key(&edge.to) {
|
if !in_nodes.contains_key(&edge.to) {
|
||||||
new_edge.detached = true;
|
new_edge.detached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(e) = new_edges.get_mut(i) {
|
if let Some(e) = new_edges.get_mut(&connection_key) {
|
||||||
*e = new_edge;
|
*e = new_edge;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create connections for each link
|
// Create connections for each link
|
||||||
for link in &node.links {
|
for link in &node.links {
|
||||||
new_edges.push(Edge {
|
new_edges.insert(
|
||||||
from: key.clone(),
|
link.clone(),
|
||||||
to: link.clone(),
|
Edge {
|
||||||
anchor: String::default(),
|
|
||||||
detached: !old_nodes.clone().contains_key(link),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create connections for each anchor
|
|
||||||
let parsed_anchors =
|
|
||||||
tokens.iter().filter(|t| matches!(t, Token::Anchor(_)));
|
|
||||||
|
|
||||||
let mut anchors: Vec<Anchor> = vec![];
|
|
||||||
for anchor in parsed_anchors {
|
|
||||||
if let Token::Anchor(a) = anchor {
|
|
||||||
anchors.push(*a.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for anchor in anchors {
|
|
||||||
if let Some(anchor_node) = anchor.node() {
|
|
||||||
new_edges.push(Edge {
|
|
||||||
from: key.clone(),
|
from: key.clone(),
|
||||||
to: anchor_node.id,
|
to: link.clone(),
|
||||||
anchor: anchor.text(),
|
detached: !in_nodes.clone().contains_key(link),
|
||||||
detached: false,
|
},
|
||||||
});
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate empty titles with IDs
|
// Populate empty titles with IDs
|
||||||
|
|
@ -131,19 +108,64 @@ fn modulate_nodes(graph: &Graph) -> HashMap<String, Node> {
|
||||||
node.summary.clone()
|
node.summary.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Assemble new node
|
||||||
let new_node = Node {
|
let new_node = Node {
|
||||||
id: key.clone(),
|
id: key.clone(),
|
||||||
title: new_title,
|
title: new_title,
|
||||||
summary: flatten(&summary, graph),
|
summary: flatten(&summary, graph),
|
||||||
|
connections: Some(new_edges),
|
||||||
|
..node.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
first_pass_nodes.insert(key.clone(), new_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut second_pass_nodes: HashMap<String, Node> = HashMap::default();
|
||||||
|
for (key, node) in first_pass_nodes.clone() {
|
||||||
|
let first_pass_graph = Graph {
|
||||||
|
nodes: first_pass_nodes.clone(),
|
||||||
|
..graph.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse node text
|
||||||
|
let (text, tokens) = content::rich_parse(&node.text, &first_pass_graph);
|
||||||
|
|
||||||
|
// Create connections for each anchor
|
||||||
|
let parsed_anchors =
|
||||||
|
tokens.iter().filter(|t| matches!(t, Token::Anchor(_)));
|
||||||
|
|
||||||
|
let mut anchors: Vec<Anchor> = vec![];
|
||||||
|
for anchor in parsed_anchors {
|
||||||
|
if let Token::Anchor(a) = anchor {
|
||||||
|
anchors.push(*a.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_edges = node.connections.clone().unwrap_or_default();
|
||||||
|
for anchor in anchors {
|
||||||
|
if let Some(anchor_node) = anchor.node() {
|
||||||
|
new_edges.insert(
|
||||||
|
anchor_node.id.clone(),
|
||||||
|
Edge {
|
||||||
|
from: key.clone(),
|
||||||
|
to: anchor_node.id,
|
||||||
|
detached: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble new node
|
||||||
|
let new_node = Node {
|
||||||
connections: Some(new_edges),
|
connections: Some(new_edges),
|
||||||
text,
|
text,
|
||||||
..node.clone()
|
..node.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.insert(key.clone(), new_node);
|
second_pass_nodes.insert(key.clone(), new_node);
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes
|
second_pass_nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Format {
|
pub enum Format {
|
||||||
|
|
@ -183,7 +205,7 @@ fn make_incoming(nodes: &HashMap<String, Node>) -> HashMap<String, Vec<Edge>> {
|
||||||
|
|
||||||
for node in nodes.clone().into_values() {
|
for node in nodes.clone().into_values() {
|
||||||
let empty_vec: Vec<Edge> = vec![];
|
let empty_vec: Vec<Edge> = vec![];
|
||||||
for edge in &node.connections.clone().unwrap_or_default() {
|
for edge in node.connections.clone().unwrap_or_default().values() {
|
||||||
let mut edges =
|
let mut edges =
|
||||||
incoming.get(&edge.to.clone()).unwrap_or(&empty_vec).clone();
|
incoming.get(&edge.to.clone()).unwrap_or(&empty_vec).clone();
|
||||||
edges.extend_from_slice(std::slice::from_ref(edge));
|
edges.extend_from_slice(std::slice::from_ref(edge));
|
||||||
|
|
@ -194,17 +216,6 @@ fn make_incoming(nodes: &HashMap<String, Node>) -> HashMap<String, Vec<Edge>> {
|
||||||
incoming
|
incoming
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_lowercase_keys(
|
|
||||||
source_map: &HashMap<String, Node>,
|
|
||||||
) -> HashMap<String, String> {
|
|
||||||
let mut out_map: HashMap<String, String> = HashMap::default();
|
|
||||||
let keys = source_map.keys();
|
|
||||||
for key in keys {
|
|
||||||
out_map.insert(key.clone().to_lowercase(), key.clone());
|
|
||||||
}
|
|
||||||
out_map
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -220,7 +231,7 @@ mod tests {
|
||||||
"links": [],
|
"links": [],
|
||||||
"id": "JSON",
|
"id": "JSON",
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"connections": []
|
"connections": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root_node": "JSON"
|
"root_node": "JSON"
|
||||||
|
|
|
||||||
|
|
@ -484,10 +484,6 @@ Because en is defined in simple configuration files, you can add new pages easil
|
||||||
|
|
||||||
links = [ "Graph" ]
|
links = [ "Graph" ]
|
||||||
|
|
||||||
[[nodes.en.connections]]
|
|
||||||
to = "TOML"
|
|
||||||
anchor = "TOML"
|
|
||||||
|
|
||||||
[nodes.Graph]
|
[nodes.Graph]
|
||||||
text = """
|
text = """
|
||||||
A graph is a data structure composed of connected (and disconnected) nodes.
|
A graph is a data structure composed of connected (and disconnected) nodes.
|
||||||
|
|
@ -519,42 +515,49 @@ en is only possible thanks to a number of projects and people:
|
||||||
|
|
||||||
[nodes.Roadmap]
|
[nodes.Roadmap]
|
||||||
text = """
|
text = """
|
||||||
- [x] Formatting
|
- [ ] Performance
|
||||||
- [ ] Blockquotes
|
- [ ] Caching
|
||||||
- [x] Nested formatting
|
- [ ] Move more logic from Serial to Graph submodules
|
||||||
- [x] Headers
|
- [ ] Further centralize state
|
||||||
- [x] Preformatted blocks
|
- [ ] Reduce O(n) calls in the formats module
|
||||||
- [x] _Oblique_,
|
- [ ] Input syntax
|
||||||
- [x] __Underline__
|
- [ ] Invert where redirects are set
|
||||||
- [x] ~~Strikethrough~~
|
- [x] Formatting
|
||||||
- [x] *Bold*
|
- [ ] Blockquotes
|
||||||
- [x] `Inline code`
|
- [x] Nested formatting
|
||||||
- [x] Lists
|
- [x] Headers
|
||||||
- [x] Nested lists
|
- [x] Preformatted blocks
|
||||||
- [x] Checkboxes
|
- [x] _Oblique_,
|
||||||
- [x] Move this roadmap to en
|
- [x] __Underline__
|
||||||
- [ ] Caching
|
- [x] ~~Strikethrough~~
|
||||||
- [ ] Invert where redirects are set
|
- [x] *Bold*
|
||||||
|
- [x] `Inline code`
|
||||||
|
- [x] Lists
|
||||||
|
- [x] Nested lists
|
||||||
|
- [x] Checkboxes
|
||||||
|
- [x] Move this roadmap to en
|
||||||
|
- [ ] Special sections
|
||||||
|
- [ ] Definition (implies metadata `has_definition`)
|
||||||
|
- [ ] See also (implies a kind of connection, e.g. `related`)
|
||||||
|
- [ ] Not to be confused with (implies a kind of connection)
|
||||||
|
- [ ] Contrast with (implies a kind of connection)
|
||||||
|
- [ ] Example (implies metadata `has_example`)
|
||||||
|
- [ ] References/influences (implies metadata `has_references`)
|
||||||
- [ ] Meta-awareness
|
- [ ] Meta-awareness
|
||||||
- [ ] Detached edges
|
- [ ] Detached edges
|
||||||
- [ ] Most linked to nodes
|
- [ ] Most linked to nodes
|
||||||
- [ ] Most linked from nodes
|
- [ ] Most linked from nodes
|
||||||
- [ ] Most linked to nonexistent nodes
|
- [ ] Most linked to nonexistent nodes
|
||||||
- [ ] Most linked
|
- [ ] Most linked
|
||||||
- [ ] Special sections
|
- [ ] Rendering
|
||||||
- [ ] Definition (implies metadata `has_definition`)
|
- [ ] Sorting of tree, index list and drop-down navigation
|
||||||
- [ ] See also (implies a kind of connection, e.g. `related`)
|
- [ ] Alphabetic
|
||||||
- [ ] Not to be confused with (implies a kind of connection)
|
- [ ] By most linked to
|
||||||
- [ ] Contrast with (implies a kind of connection)
|
- [ ] By most linked
|
||||||
- [ ] Example (implies metadata `has_example`)
|
- [ ] Themes
|
||||||
- [ ] References/influences (implies metadata `has_references`)
|
|
||||||
- [ ] Sorting of tree, index list and drop-down navigation
|
|
||||||
- [ ] Alphabetic
|
|
||||||
- [ ] By most linked to
|
|
||||||
- [ ] By most linked
|
|
||||||
- [x] Anchors and connections
|
- [x] Anchors and connections
|
||||||
- [x] Render detached anchors differently
|
- [x] Render detached anchors differently
|
||||||
- [ ] Count connection to a redirect as a connection to the target
|
- [x] Count connection to a redirect as a connection to the target
|
||||||
- [ ] Suffix-aware anchors
|
- [ ] Suffix-aware anchors
|
||||||
- [x] Plural anchors (`|node|s` -> `node`)
|
- [x] Plural anchors (`|node|s` -> `node`)
|
||||||
- [x] Ignore trailing punctuation
|
- [x] Ignore trailing punctuation
|
||||||
|
|
@ -572,17 +575,16 @@ text = """
|
||||||
- [ ] Specialization <-> Generalization
|
- [ ] Specialization <-> Generalization
|
||||||
- [ ] Custom connection kinds
|
- [ ] Custom connection kinds
|
||||||
- [x] External anchors
|
- [x] External anchors
|
||||||
|
- [ ] I/O formats
|
||||||
|
- [ ] Output
|
||||||
|
- [ ] Add separate TOML endpoints for pre/postprocessed
|
||||||
|
- [ ] Render to filesystem
|
||||||
|
- [ ] Single-page rendering
|
||||||
|
- [ ] Input
|
||||||
|
- [ ] Frontmatter format
|
||||||
|
- [ ] Multi-file graphs
|
||||||
|
- [ ] Multi-graph
|
||||||
- [ ] Full-text search
|
- [ ] Full-text search
|
||||||
- [ ] Begin centralizing state
|
|
||||||
- [ ] Reduce O(n) calls in the formats module
|
|
||||||
- [ ] Alternative rendering modes
|
|
||||||
- [ ] Render to filesystem
|
|
||||||
- [ ] Single-page rendering
|
|
||||||
- [ ] Input formats
|
|
||||||
- [ ] Frontmatter format
|
|
||||||
- [ ] Multi-file graphs
|
|
||||||
- [ ] Multi-graph
|
|
||||||
- [ ] Themes
|
|
||||||
|
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
|
|
@ -604,3 +606,14 @@ navbar_search = true
|
||||||
footer_text = """
|
footer_text = """
|
||||||
made by jutty|https://jutty.dev • acknowledgments|Acknowledgments • |source code|https://codeberg.org/jutty/en
|
made by jutty|https://jutty.dev • acknowledgments|Acknowledgments • |source code|https://codeberg.org/jutty/en
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
[nodes.t0]
|
||||||
|
text = """
|
||||||
|
*t0* is a node linked to |t1|
|
||||||
|
"""
|
||||||
|
|
||||||
|
[nodes.t1]
|
||||||
|
text = """
|
||||||
|
*t1* is a node linked to |T0|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,20 @@
|
||||||
</div>
|
</div>
|
||||||
{{ node.text | safe }}
|
{{ node.text | safe }}
|
||||||
</section>
|
</section>
|
||||||
{% if node.connections or incoming %}
|
{% if connections or incoming %}
|
||||||
<aside>
|
<aside>
|
||||||
<hr>
|
<hr>
|
||||||
<h2>Connections</h2>
|
<h2>Connections</h2>
|
||||||
{% if node.connections %}
|
{% if connections %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for connection in node.connections | filter(attribute="detached", value=false) %}
|
{% for connection in connections | filter(attribute="detached", value=false) %}
|
||||||
<li>
|
<li>
|
||||||
<strong>{{node.id}}</strong>
|
<strong>{{node.id}}</strong>
|
||||||
»
|
»
|
||||||
<a href="/node/{{connection.to}}">{{connection.to}}</a>
|
<a href="/node/{{connection.to}}">{{connection.to}}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for connection in node.connections | filter(attribute="detached", value=true) %}
|
{% for connection in connections | filter(attribute="detached", value=true) %}
|
||||||
<li>
|
<li>
|
||||||
<strong>{{node.id}}</strong>
|
<strong>{{node.id}}</strong>
|
||||||
»
|
»
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
{% if root_node.connections %}
|
{% if root_node.connections %}
|
||||||
{% if config.tree_node_summary %}<li><strong>Connections</strong>
|
{% if config.tree_node_summary %}<li><strong>Connections</strong>
|
||||||
<ul>{% endif %}
|
<ul>{% endif %}
|
||||||
{% for connection in root_node.connections %}
|
{% for _, connection in root_node.connections %}
|
||||||
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
|
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if config.tree_node_summary %}</ul>
|
{% if config.tree_node_summary %}</ul>
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
{% if node.connections %}
|
{% if node.connections %}
|
||||||
{% if config.tree_node_summary %}<li><strong>Connections</strong>
|
{% if config.tree_node_summary %}<li><strong>Connections</strong>
|
||||||
<ul>{% endif %}
|
<ul>{% endif %}
|
||||||
{% for connection in node.connections %}
|
{% for _, connection in node.connections %}
|
||||||
{% if not connection.detached %}
|
{% if not connection.detached %}
|
||||||
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
|
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue