diff --git a/README.md b/README.md
index c470219..08107a5 100644
--- a/README.md
+++ b/README.md
@@ -10,13 +10,14 @@ You can learn more and see what en looks like by visiting the [homepage](https:/
## Roadmap
+- [ ] Strip/render some syntax in Tree text preview
- [ ] Richer text formatting
- [x] Headers
- - [ ] Code blocks
+ - [x] Preformatted blocks
- [ ] Inline code
- - [ ] Anchor rendering
+ - [x] Anchor rendering
- [ ] Automatic anchors
- - [ ] External anchors
+ - [x] External anchors
- [ ] Bold, italics, underline, strikethrough
- [ ] Lists
- [ ] Checkboxes
@@ -34,8 +35,8 @@ You can learn more and see what en looks like by visiting the [homepage](https:/
- [ ] Reduce O(n) calls in the formats module
- [ ] Add tests
- [ ] Multi-file graphs
-- [ ] Themes
- [ ] Multi-graph
+- [ ] Themes
- [x] Array syntax for lightweight connections
- [x] Automatic IDs
- [x] Automatic titles
diff --git a/src/handlers/fixed.rs b/src/handlers/fixed.rs
index b14ec66..7b372b4 100644
--- a/src/handlers/fixed.rs
+++ b/src/handlers/fixed.rs
@@ -11,6 +11,7 @@ use crate::handlers;
/// # Panics
/// Will panic if file read fails.
+#[expect(clippy::unused_async)]
pub async fn file(file_path: &str, content_type: &str) -> Response
{
let content = match std::fs::read(file_path) {
Ok(s) => s,
diff --git a/src/handlers/graph.rs b/src/handlers/graph.rs
index c954a4c..2066e73 100644
--- a/src/handlers/graph.rs
+++ b/src/handlers/graph.rs
@@ -1,8 +1,6 @@
use axum::{body::Body, extract::Path, http::Response};
-use crate::syntax::content::parsers::line::elements::paragraph::Paragraph;
use crate::syntax::content;
-use crate::syntax::content::parsers::compound::elements::literal::Literal;
use crate::{formats::populate_graph, handlers, types::Node};
pub async fn node(Path(id): Path) -> Response {
@@ -17,7 +15,7 @@ pub async fn node(Path(id): Path) -> Response {
context.insert("incoming", &graph.incoming.get(&id));
context.insert("config", &graph.meta.config.parse_text());
- let out_text = content::parse::(&node.text);
+ let out_text = content::parse(&node.text);
context.insert("text", &out_text);
let not_found = *node == empty_node;
diff --git a/src/handlers/navigation.rs b/src/handlers/navigation.rs
index 548bed3..9a6ea26 100644
--- a/src/handlers/navigation.rs
+++ b/src/handlers/navigation.rs
@@ -5,18 +5,7 @@ use axum::{
Form,
};
-use crate::{
- formats::populate_graph,
- handlers,
- syntax::content::{
- self,
- parsers::{
- line::elements::{paragraph::Paragraph, span::Span},
- compound::elements::literal::Literal,
- },
- },
- types::{Config, Node},
-};
+use crate::{formats::populate_graph, handlers, types::Node};
#[expect(clippy::unused_async)]
pub async fn page(template: &str) -> Response {
diff --git a/src/handlers/template.rs b/src/handlers/template.rs
index 63ffe28..7934f0e 100644
--- a/src/handlers/template.rs
+++ b/src/handlers/template.rs
@@ -3,7 +3,7 @@ use axum::{
http::{header, Response, StatusCode},
};
-use crate::handlers::raw::make_response;
+use crate::{prelude::*, handlers::raw::make_response};
pub(in crate::handlers) fn by_filename(
name: &str,
@@ -39,8 +39,8 @@ pub(in crate::handlers) fn render(
Ok(t) => t,
Err(e) => {
let early_error_message = format!("{e:#?}");
- crate::dev::log(&by_filename, &early_error_message);
- return (emergency_wrap(&e), 500)
+ log!("{}", early_error_message);
+ return (emergency_wrap(&e), 500);
},
};
@@ -79,7 +79,8 @@ pub(in crate::handlers) fn render(
}
fn emergency_wrap(message: &tera::Error) -> String {
- format!(r#"
+ format!(
+ r#"
Pre-Templating Error
@@ -105,5 +106,6 @@ fn emergency_wrap(message: &tera::Error) -> String {
- "#)
+ "#
+ )
}
diff --git a/src/syntax/arguments.rs b/src/syntax/arguments.rs
index 7ae13e7..0c55e4e 100644
--- a/src/syntax/arguments.rs
+++ b/src/syntax/arguments.rs
@@ -1,5 +1,7 @@
use std::path::PathBuf;
+use crate::prelude::*;
+
#[derive(Clone, Debug, Default)]
pub struct Arguments {
pub hostname: String,
@@ -47,10 +49,7 @@ fn parse(defaults: &Arguments, args: &[String]) -> Arguments {
} else if argument.eq("-g") || argument.eq("--graph") {
out_args.graph_path = PathBuf::from(parameter);
} else {
- crate::dev::log(
- &parse,
- &format!("Dropped unrecognized argument {argument}"),
- );
+ log!("Dropped unrecognized argument {argument}");
}
} else {
panic!("Argument {arg:?} has no corresponding value")
diff --git a/src/syntax/content.rs b/src/syntax/content.rs
index 0235c45..d885392 100644
--- a/src/syntax/content.rs
+++ b/src/syntax/content.rs
@@ -1,10 +1,6 @@
-use token::{Token};
-use parsers::{line::Line, word::Word};
-use lexeme::Lexeme;
+use parser::{token::Token, lexeme::Lexeme};
-mod token;
-pub mod lexeme;
-pub mod parsers;
+pub mod parser;
pub trait Parseable: Into {
fn probe(lexeme: &Lexeme) -> bool;
@@ -16,22 +12,6 @@ type Probe = fn(&Lexeme) -> bool;
type Lexer = fn(&Lexeme) -> Token;
type LexMap<'lm> = &'lm [(Probe, Lexer)];
-fn make_lexmap(base: LexMap) -> Vec<(Probe, Lexer)> {
- let mut vector: Vec<(Probe, Lexer)> = base.to_vec();
-
- fn adapter(lex: &Lexeme) -> Token {
- D::lex(lex).into()
- }
-
- vector.push((DefaultToken::probe, adapter::));
- vector
-}
-
-pub fn parse(
- text: &str,
-) -> String {
- let escaped_text = tera::escape_html(text);
- parsers::line::parser::read::(
- &parsers::word::parser::read::(&escaped_text),
- )
+pub fn parse(text: &str) -> String {
+ parser::read(text)
}
diff --git a/src/syntax/content/lexeme.rs b/src/syntax/content/lexeme.rs
deleted file mode 100644
index 398c606..0000000
--- a/src/syntax/content/lexeme.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use super::parsers::{line::Line, word::Word};
-
-#[derive(Clone)]
-pub enum Lexeme {
- Line(Line),
- Word(Word),
-}
-
-impl Lexeme {
- pub fn to_raw(&self) -> String {
- match *self {
- Lexeme::Line(ref d) => d.raw.clone(),
- Lexeme::Word(ref d) => d.raw.clone(),
- }
- }
-
- pub fn to_vec(self) -> Vec {
- self.to_raw().split(' ').map(str::to_string).collect()
- }
-
- pub fn first(self) -> Option {
- self.to_vec().first().map(String::to_owned)
- }
-}
diff --git a/src/syntax/content/parser.rs b/src/syntax/content/parser.rs
new file mode 100644
index 0000000..45c435f
--- /dev/null
+++ b/src/syntax/content/parser.rs
@@ -0,0 +1,186 @@
+use std::slice::Iter;
+
+use crate::prelude::*;
+use super::{Parseable as _, Token, LexMap};
+use token::{
+ anchor::Anchor, linebreak::LineBreak, paragraph::Paragraph, header::Header,
+ preformat::PreFormat, literal::Literal,
+};
+use lexeme::{Lexeme, compound::Compound};
+
+pub mod token;
+pub mod lexeme;
+
+const LEXMAP: LexMap = &[
+ (Anchor::probe, |word| Token::Anchor(Anchor::lex(word))),
+ (LineBreak::probe, |word| {
+ Token::LineBreak(LineBreak::lex(word))
+ }),
+ (Literal::probe, |word| Token::Literal(Literal::lex(word))),
+];
+
+enum Context {
+ None,
+ Paragraph,
+ Header(u8),
+ PreFormat,
+}
+
+fn lex(text: &str, map: LexMap) -> Vec {
+ let mut tokens: Vec = Vec::new();
+ let mut state = Context::None;
+
+ let splits = split(text);
+ let mut iter = splits.iter();
+ while let Some(word) = iter.next() {
+ let compound = cluster(word, &mut iter);
+ let lexeme = Lexeme::Compound(compound);
+
+ match state {
+ Context::None => {
+ if Header::probe(&lexeme) {
+ let header = Header::lex(&lexeme);
+ state = Context::Header(header.get_level());
+ tokens.push(Token::Header(header));
+ continue;
+ } else if PreFormat::probe(&lexeme) {
+ tokens.push(Token::PreFormat(PreFormat::new(true)));
+ state = Context::PreFormat;
+ continue;
+ } else if Paragraph::probe(&lexeme) {
+ tokens.push(Token::Paragraph(Paragraph::new(true)));
+ state = Context::Paragraph;
+ }
+ },
+ Context::Paragraph => {
+ if word == "\n" {
+ tokens.push(Token::Paragraph(Paragraph::new(false)));
+ state = Context::None;
+ }
+ },
+ Context::Header(n) => {
+ if word == "\n" {
+ tokens.push(Token::Header(Header::from_u8(n, false)));
+ state = Context::None;
+ }
+ },
+ Context::PreFormat => {
+ if PreFormat::probe(&lexeme) {
+ tokens.push(Token::PreFormat(PreFormat::new(false)));
+ state = Context::None;
+ continue;
+ }
+ },
+ }
+
+ for &(ref probe, lex) in map {
+ if probe(&lexeme) {
+ tokens.push(lex(&lexeme));
+ break;
+ }
+ }
+ }
+
+ tokens
+}
+
+fn split(text: &str) -> Vec {
+ text.replace("\n", " \n ")
+ .split(' ')
+ .map(str::to_string)
+ .collect()
+}
+
+// this could be eliminated if space were a token
+fn join<'i, Iterator>(rendered_tokens: Iterator) -> String
+where
+ Iterator: IntoIterator- ,
+{
+ fn stick(current: &str, next: &str) -> bool {
+ // this could be in a dedicated type
+ fn is_tag(s: &str) -> bool {
+ s.starts_with("<") && s.ends_with('>')
+ }
+ fn is_opening(s: &str) -> bool {
+ is_tag(s) && !s.contains("")
+ }
+ fn is_closing(s: &str) -> bool {
+ is_tag(s) && s.contains("")
+ }
+ fn is_inline(s: &str) -> bool {
+ is_tag(s) && s.starts_with(" String {
+ let rendered: Vec = tokens.iter().map(Token::render).collect();
+
+ join(rendered.iter().map(String::as_str))
+}
+
+fn cluster<'c>(word: &str, iter: &mut Iter<'c, String>) -> Compound {
+ if word.starts_with('|') {
+ log!("Found opener {word}");
+ let mut parts = vec![word];
+
+ if let Some(first) = parts.first()
+ && first.ends_with('|')
+ {
+ log!("Returning atomic cluster");
+ Compound::new(&parts.join(" "))
+ } else {
+ log!("Seeking a boundary");
+ for next_raw in iter {
+ if next_raw.contains('|') {
+ log!("Found end of cluster {next_raw:?}");
+ parts.push(next_raw);
+ break;
+ } else {
+ parts.push(next_raw);
+ log!("Onto next word from {next_raw}");
+ }
+ }
+ log!("Returning cluster {parts:?}");
+
+ Compound::new(&parts.join(" "))
+ }
+ } else {
+ Compound::new(word)
+ }
+}
+
+pub(super) fn read(text: &str) -> String {
+ parse(&lex(text, LEXMAP))
+}
diff --git a/src/syntax/content/parser/lexeme.rs b/src/syntax/content/parser/lexeme.rs
new file mode 100644
index 0000000..ef94ea6
--- /dev/null
+++ b/src/syntax/content/parser/lexeme.rs
@@ -0,0 +1,39 @@
+#[derive(Clone)]
+pub enum Lexeme {
+ Compound(compound::Compound),
+}
+
+pub mod compound;
+
+impl Lexeme {
+ pub fn to_raw(&self) -> String {
+ match *self {
+ Lexeme::Compound(ref d) => d.raw.clone(),
+ }
+ }
+
+ /// # Panics
+ /// Panics if number of chars for a single lexeme exceeds `i2::MAX`
+ pub fn count_char(&self, c: char) -> i32 {
+ let count = self.to_raw().chars().filter(|&n| n == c).count();
+ match i32::try_from(count) {
+ Ok(i) => i,
+ Err(e) => {
+ panic!("Wild char number {count} is a bit much: {e:#?}");
+ },
+ }
+ }
+
+ pub fn split_chars(&self) -> Vec {
+ let vector: Vec = self.to_raw().chars().collect();
+ vector
+ }
+
+ pub fn split_words(self) -> Vec {
+ self.to_raw().split(' ').map(str::to_string).collect()
+ }
+
+ pub fn first(self) -> Option {
+ self.split_words().first().map(String::to_owned)
+ }
+}
diff --git a/src/syntax/content/parser/lexeme/compound.rs b/src/syntax/content/parser/lexeme/compound.rs
new file mode 100644
index 0000000..1f30cd1
--- /dev/null
+++ b/src/syntax/content/parser/lexeme/compound.rs
@@ -0,0 +1,12 @@
+#[derive(Clone)]
+pub struct Compound {
+ pub raw: String,
+}
+
+impl Compound {
+ pub fn new(text: &str) -> Compound {
+ Compound {
+ raw: text.to_owned(),
+ }
+ }
+}
diff --git a/src/syntax/content/parser/token.rs b/src/syntax/content/parser/token.rs
new file mode 100644
index 0000000..897239d
--- /dev/null
+++ b/src/syntax/content/parser/token.rs
@@ -0,0 +1,75 @@
+use crate::syntax::content::Parseable as _;
+
+pub mod literal;
+pub mod anchor;
+pub mod linebreak;
+pub mod paragraph;
+pub mod span;
+pub mod header;
+pub mod preformat;
+
+pub enum Token {
+ Anchor(anchor::Anchor),
+ Header(header::Header),
+ LineBreak(linebreak::LineBreak),
+ Literal(literal::Literal),
+ Paragraph(paragraph::Paragraph),
+ PreFormat(preformat::PreFormat),
+ Span(span::Span),
+}
+
+impl Token {
+ pub fn render(&self) -> String {
+ match *self {
+ Token::Anchor(ref d) => d.render(),
+ Token::Header(ref d) => d.render(),
+ Token::LineBreak(ref d) => d.render(),
+ Token::Literal(ref d) => d.render(),
+ Token::Paragraph(ref d) => d.render(),
+ Token::PreFormat(ref d) => d.render(),
+ Token::Span(ref d) => d.render(),
+ }
+ }
+}
+
+impl From for Token {
+ fn from(d: paragraph::Paragraph) -> Token {
+ Token::Paragraph(d)
+ }
+}
+
+impl From for Token {
+ fn from(d: header::Header) -> Token {
+ Token::Header(d)
+ }
+}
+
+impl From for Token {
+ fn from(d: span::Span) -> Token {
+ Token::Span(d)
+ }
+}
+
+impl From for Token {
+ fn from(d: literal::Literal) -> Token {
+ Token::Literal(d)
+ }
+}
+
+impl From for Token {
+ fn from(d: anchor::Anchor) -> Token {
+ Token::Anchor(d)
+ }
+}
+
+impl From for Token {
+ fn from(d: linebreak::LineBreak) -> Token {
+ Token::LineBreak(d)
+ }
+}
+
+impl From for Token {
+ fn from(d: preformat::PreFormat) -> Token {
+ Token::PreFormat(d)
+ }
+}
diff --git a/src/syntax/content/parser/token/anchor.rs b/src/syntax/content/parser/token/anchor.rs
new file mode 100644
index 0000000..a8b5c87
--- /dev/null
+++ b/src/syntax/content/parser/token/anchor.rs
@@ -0,0 +1,68 @@
+use std::fmt::Display;
+use crate::syntax::content::{Parseable, parser::lexeme::Lexeme};
+
+pub struct Anchor {
+ text: String,
+ destination: String,
+}
+
+impl Parseable for Anchor {
+ fn probe(lexeme: &Lexeme) -> bool {
+ let pipe_count = lexeme.count_char('|');
+ let chars = lexeme.split_chars();
+ let c1 = *match chars.first() {
+ Some(c) => c,
+ None => return false,
+ };
+ let cn = *match chars.last() {
+ Some(c) => c,
+ None => return false,
+ };
+
+ if !(1_i32..=3_i32).contains(&pipe_count) {
+ return false;
+ }
+ if lexeme.to_raw().matches("||").count() > 0 {
+ return false;
+ }
+
+ if pipe_count == 1 {
+ c1 != '|' && cn != '|'
+ } else if pipe_count == 2 {
+ c1 == '|' && cn != '|'
+ } else if pipe_count == 3 {
+ c1 == '|' && cn == '|'
+ } else {
+ false
+ }
+ }
+
+ fn lex(lexeme: &Lexeme) -> Anchor {
+ let parts: Vec = lexeme
+ .to_raw()
+ .split('|')
+ .filter(|s| !s.is_empty())
+ .map(str::to_string)
+ .collect();
+
+ assert!(parts.len() == 2, "Parts should always be 2: {parts:?}");
+
+ let text = parts.first().unwrap_or_else(|| unreachable!());
+ let destination = parts.get(1).unwrap_or_else(|| unreachable!());
+
+ Anchor {
+ text: text.to_owned(),
+ destination: destination.to_owned(),
+ }
+ }
+
+ fn render(&self) -> String {
+ format!(r#"{}"#, &self.destination, &self.text)
+ }
+}
+
+impl Display for Anchor {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "Anchor: <{}> to <{}>", &self.text, &self.destination)
+ }
+}
diff --git a/src/syntax/content/parser/token/header.rs b/src/syntax/content/parser/token/header.rs
new file mode 100644
index 0000000..1c13fbf
--- /dev/null
+++ b/src/syntax/content/parser/token/header.rs
@@ -0,0 +1,137 @@
+use crate::{
+ prelude::*,
+ syntax::content::{Parseable, Lexeme},
+};
+use std::fmt::Display;
+
+pub struct Header {
+ open: Option,
+ level: Level,
+}
+
+impl Header {
+ pub fn new(level: Level, open: bool) -> Header {
+ Header {
+ level,
+ open: Some(open),
+ }
+ }
+
+ pub fn from_u8(level: u8, open: bool) -> Header {
+ Header {
+ level: Level::from_u8(level),
+ open: Some(open),
+ }
+ }
+
+ pub fn get_level(&self) -> u8 {
+ match self.level {
+ Level::One => 1,
+ Level::Two => 2,
+ Level::Three => 3,
+ Level::Four => 4,
+ Level::Five => 5,
+ Level::Six => 6,
+ }
+ }
+}
+
+impl Parseable for Header {
+ fn probe(lexeme: &Lexeme) -> bool {
+ if lexeme
+ .split_chars()
+ .into_iter()
+ .filter(|e| *e != '#')
+ .count()
+ == 0
+ {
+ let level = lexeme.to_raw().len();
+ lexeme.clone().split_words().len() == 1 && level > 0 && level <= 6
+ } else {
+ false
+ }
+ }
+
+ fn lex(lexeme: &Lexeme) -> Header {
+ Header::new(lexeme.to_raw().len().into(), true)
+ }
+
+ fn render(&self) -> String {
+ if let Some(open) = self.open {
+ if open {
+ format!("", &self.level)
+ } else {
+ format!("", &self.level)
+ }
+ } else {
+ panic!("Attempt to render a header tag while open state is unknown")
+ }
+ }
+}
+
+impl Display for Header {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ if let Some(open) = self.open {
+ if open {
+ write!(f, "Level {} Open Header", self.level)
+ } else {
+ write!(f, "Level {} Closed Header", self.level)
+ }
+ } else {
+ write!(f, "Level {} Header (Unknown open state)", self.level)
+ }
+ }
+}
+
+pub enum Level {
+ One,
+ Two,
+ Three,
+ Four,
+ Five,
+ Six,
+}
+
+impl Level {
+ fn from_u8(u: u8) -> Level {
+ if u <= 1 {
+ Level::One
+ } else if u == 2 {
+ Level::Two
+ } else if u == 3 {
+ Level::Three
+ } else if u == 4 {
+ Level::Four
+ } else if u == 5 {
+ Level::Five
+ } else {
+ Level::Six
+ }
+ }
+}
+
+impl From for Level {
+ fn from(z: usize) -> Level {
+ let u8 = match u8::try_from(z) {
+ Ok(u) => u,
+ Err(e) => {
+ log!("Truncating header level {z} to 6: {e:?}");
+ 6_u8
+ },
+ };
+ Level::from_u8(u8)
+ }
+}
+
+impl Display for Level {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match *self {
+ Level::One => write!(f, "1"),
+ Level::Two => write!(f, "2"),
+ Level::Three => write!(f, "3"),
+ Level::Four => write!(f, "4"),
+ Level::Five => write!(f, "5"),
+ Level::Six => write!(f, "6"),
+ }
+ }
+}
diff --git a/src/syntax/content/parser/token/linebreak.rs b/src/syntax/content/parser/token/linebreak.rs
new file mode 100644
index 0000000..8fb6d52
--- /dev/null
+++ b/src/syntax/content/parser/token/linebreak.rs
@@ -0,0 +1,26 @@
+use std::fmt::Display;
+use crate::{
+ syntax::content::{Parseable, parser::lexeme::Lexeme},
+};
+
+pub struct LineBreak {}
+
+impl Parseable for LineBreak {
+ fn probe(lexeme: &Lexeme) -> bool {
+ lexeme.to_raw() == "\n"
+ }
+
+ fn lex(_lexeme: &Lexeme) -> LineBreak {
+ LineBreak {}
+ }
+
+ fn render(&self) -> String {
+ "\n".to_owned()
+ }
+}
+
+impl Display for LineBreak {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "Line Break")
+ }
+}
diff --git a/src/syntax/content/parsers/word/elements/literal.rs b/src/syntax/content/parser/token/literal.rs
similarity index 68%
rename from src/syntax/content/parsers/word/elements/literal.rs
rename to src/syntax/content/parser/token/literal.rs
index 3c8ee78..131af39 100644
--- a/src/syntax/content/parsers/word/elements/literal.rs
+++ b/src/syntax/content/parser/token/literal.rs
@@ -1,18 +1,18 @@
use std::fmt::Display;
-use crate::syntax::content::{Parseable, lexeme::Lexeme};
+use crate::syntax::content::{Parseable, parser::lexeme::Lexeme};
pub struct Literal {
text: String,
}
impl Parseable for Literal {
- fn probe(lexeme: &Lexeme) -> bool {
- !lexeme.to_raw().is_empty()
+ fn probe(_lexeme: &Lexeme) -> bool {
+ true
}
fn lex(lexeme: &Lexeme) -> Literal {
Literal {
- text: lexeme.to_raw().trim().to_owned(),
+ text: lexeme.to_raw(),
}
}
diff --git a/src/syntax/content/parser/token/paragraph.rs b/src/syntax/content/parser/token/paragraph.rs
new file mode 100644
index 0000000..e8c8543
--- /dev/null
+++ b/src/syntax/content/parser/token/paragraph.rs
@@ -0,0 +1,53 @@
+use std::fmt::Display;
+use crate::syntax::content::{Parseable, parser::lexeme::Lexeme};
+
+pub struct Paragraph {
+ open: Option,
+}
+
+impl Paragraph {
+ pub fn new(open: bool) -> Paragraph {
+ Paragraph { open: Some(open) }
+ }
+}
+
+impl Parseable for Paragraph {
+ fn probe(lexeme: &Lexeme) -> bool {
+ // lexeme for paragraph is any non-whitespace, parser knows the context
+ let raw = lexeme.to_raw();
+ let trimmed = raw.trim();
+ !trimmed.is_empty() && trimmed != "\n"
+ }
+
+ fn lex(_lexeme: &Lexeme) -> Paragraph {
+ Paragraph { open: None }
+ }
+
+ fn render(&self) -> String {
+ if let Some(open) = self.open {
+ if open {
+ "
".to_owned()
+ } else {
+ "
".to_owned()
+ }
+ } else {
+ panic!(
+ "Attempt to render a paragraph tag while open state is unknown"
+ )
+ }
+ }
+}
+
+impl Display for Paragraph {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ if let Some(open) = self.open {
+ if open {
+ write!(f, "Open Paragraph")
+ } else {
+ write!(f, "Closed Paragraph")
+ }
+ } else {
+ write!(f, "Unitialized Paragraph (Unknown open state)")
+ }
+ }
+}
diff --git a/src/syntax/content/parser/token/preformat.rs b/src/syntax/content/parser/token/preformat.rs
new file mode 100644
index 0000000..af50fbd
--- /dev/null
+++ b/src/syntax/content/parser/token/preformat.rs
@@ -0,0 +1,43 @@
+use crate::{
+ syntax::content::{Parseable, Lexeme},
+};
+
+pub struct PreFormat {
+ open: Option,
+}
+
+impl PreFormat {
+ pub fn new(open: bool) -> PreFormat {
+ PreFormat { open: Some(open) }
+ }
+}
+
+impl Parseable for PreFormat {
+ fn probe(lexeme: &Lexeme) -> bool {
+ let chars = lexeme.split_chars();
+
+ if let Some(first_char) = chars.first() {
+ *first_char == '`'
+ } else {
+ false
+ }
+ }
+
+ fn lex(_lexeme: &Lexeme) -> PreFormat {
+ PreFormat { open: None }
+ }
+
+ fn render(&self) -> String {
+ if let Some(o) = self.open {
+ if o {
+ "".to_owned()
+ } else {
+ "".to_owned()
+ }
+ } else {
+ panic!(
+ "Attempt to render a preformat tag while open state is unknown"
+ )
+ }
+ }
+}
diff --git a/src/syntax/content/parser/token/span.rs b/src/syntax/content/parser/token/span.rs
new file mode 100644
index 0000000..961e72d
--- /dev/null
+++ b/src/syntax/content/parser/token/span.rs
@@ -0,0 +1,49 @@
+use std::fmt::Display;
+use crate::syntax::content::{Parseable, parser::lexeme::Lexeme};
+
+pub struct Span {
+ open: Option,
+}
+
+impl Span {
+ pub fn new(open: bool) -> Span {
+ Span { open: Some(open) }
+ }
+}
+
+impl Parseable for Span {
+ fn probe(_lexeme: &Lexeme) -> bool {
+ // there is no lexeme for span
+ false
+ }
+
+ fn lex(_lexeme: &Lexeme) -> Span {
+ Span { open: None }
+ }
+
+ fn render(&self) -> String {
+ if let Some(open) = self.open {
+ if open {
+ "".to_owned()
+ } else {
+ "".to_owned()
+ }
+ } else {
+ panic!("Attempt to render a span tag while open state is unknown")
+ }
+ }
+}
+
+impl Display for Span {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ if let Some(open) = self.open {
+ if open {
+ write!(f, "Open Span")
+ } else {
+ write!(f, "Closed Span")
+ }
+ } else {
+ write!(f, "Span (Unknown open state)")
+ }
+ }
+}
diff --git a/src/syntax/content/parsers.rs b/src/syntax/content/parsers.rs
deleted file mode 100644
index 06cc152..0000000
--- a/src/syntax/content/parsers.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub mod line;
-pub mod word;
diff --git a/src/syntax/content/parsers/line.rs b/src/syntax/content/parsers/line.rs
deleted file mode 100644
index e8c7c1b..0000000
--- a/src/syntax/content/parsers/line.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-use crate::syntax::content::lexeme::Lexeme;
-
-pub mod parser;
-pub mod elements;
-
-#[derive(Clone)]
-pub struct Line {
- pub raw: String,
- pub first: String,
-}
-
-impl Line {
- pub fn new(text: &str) -> Line {
- let vec: Vec<&str> = text.split(" ").collect();
-
- Line {
- raw: text.to_owned(),
- first: vec.first().unwrap_or_else(|| unreachable!()).to_string(),
- }
- }
-}
-
-impl From for Line {
- fn from(lexeme: Lexeme) -> Line {
- match lexeme {
- Lexeme::Word(w) => Line {
- raw: w.raw.clone(),
- first: w.raw.split(' ').next().unwrap_or_default().to_owned(),
- },
- Lexeme::Line(l) => l,
- }
- }
-}
diff --git a/src/syntax/content/parsers/line/elements.rs b/src/syntax/content/parsers/line/elements.rs
deleted file mode 100644
index b00cc5f..0000000
--- a/src/syntax/content/parsers/line/elements.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod header;
-pub mod paragraph;
-pub mod span;
diff --git a/src/syntax/content/parsers/line/elements/header.rs b/src/syntax/content/parsers/line/elements/header.rs
deleted file mode 100644
index a26b638..0000000
--- a/src/syntax/content/parsers/line/elements/header.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use crate::{
- dev::log,
- syntax::content::{Parseable, Lexeme},
-};
-use std::fmt::Display;
-
-enum Level {
- One,
- Two,
- Three,
- Four,
- Five,
- Six,
-}
-
-impl Display for Level {
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
- match *self {
- Level::One => write!(f, "1"),
- Level::Two => write!(f, "2"),
- Level::Three => write!(f, "3"),
- Level::Four => write!(f, "4"),
- Level::Five => write!(f, "5"),
- Level::Six => write!(f, "6"),
- }
- }
-}
-
-pub struct Header {
- level: Level,
- text: String,
-}
-
-impl Header {
- fn new(level: usize, text: &str) -> Header {
- Header {
- level: match level {
- 1 => Level::One,
- 2 => Level::Two,
- 3 => Level::Three,
- 4 => Level::Four,
- 5 => Level::Five,
- 6 => Level::Six,
- _ => {
- panic!("Attempted to construct a header with invalid level")
- },
- },
- text: text.to_owned(),
- }
- }
-}
-
-impl Parseable for Header {
- fn probe(lexeme: &Lexeme) -> bool {
- let first = lexeme.clone().first().unwrap_or_default();
- !first.trim().is_empty()
- && first.replace("#", "").is_empty()
- && first.len() <= 6
- }
-
- fn lex(lexeme: &Lexeme) -> Header {
- let first = lexeme.clone().first().unwrap_or_else(|| unreachable!());
- let header_level = &first.len();
- log(&Header::lex, &format!("Header level is {header_level}"));
-
- let header_text = lexeme.to_raw().replace(&first, "");
-
- Header::new(*header_level, &header_text)
- }
-
- fn render(&self) -> String {
- format!("{}", &self.level, self.text)
- }
-}
-
-impl Display for Header {
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
- write!(f, "Level {} Header: <{}>", &self.level, self.text)
- }
-}
diff --git a/src/syntax/content/parsers/line/elements/paragraph.rs b/src/syntax/content/parsers/line/elements/paragraph.rs
deleted file mode 100644
index 31ee35c..0000000
--- a/src/syntax/content/parsers/line/elements/paragraph.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-use std::fmt::Display;
-use crate::syntax::content::{Parseable, lexeme::Lexeme};
-
-pub struct Paragraph {
- text: String,
-}
-
-impl Parseable for Paragraph {
- fn probe(lexeme: &Lexeme) -> bool {
- !lexeme.to_raw().trim().is_empty()
- }
-
- fn lex(lexeme: &Lexeme) -> Paragraph {
- Paragraph {
- text: lexeme.to_raw().trim().to_owned(),
- }
- }
-
- fn render(&self) -> String {
- format!("{}
", &self.text)
- }
-}
-
-impl Display for Paragraph {
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
- write!(f, "Paragraph: <{}>", &self.text)
- }
-}
diff --git a/src/syntax/content/parsers/line/elements/span.rs b/src/syntax/content/parsers/line/elements/span.rs
deleted file mode 100644
index 92ee07d..0000000
--- a/src/syntax/content/parsers/line/elements/span.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-use std::fmt::Display;
-use crate::syntax::content::{Parseable, lexeme::Lexeme};
-
-pub struct Span {
- text: String,
-}
-
-impl Parseable for Span {
- fn probe(lexeme: &Lexeme) -> bool {
- !lexeme.to_raw().trim().is_empty()
- }
-
- fn lex(lexeme: &Lexeme) -> Span {
- Span {
- text: lexeme.to_raw().trim().to_owned(),
- }
- }
-
- fn render(&self) -> String {
- format!("{}", &self.text)
- }
-}
-
-impl Display for Span {
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
- write!(f, "Span: <{}>", &self.text)
- }
-}
diff --git a/src/syntax/content/parsers/line/parser.rs b/src/syntax/content/parsers/line/parser.rs
deleted file mode 100644
index 258dbc9..0000000
--- a/src/syntax/content/parsers/line/parser.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-use crate::syntax::content::{
- LexMap, Line, Parseable, Token, parsers::line::elements::header::Header,
- make_lexmap, Lexeme,
-};
-
-const LEXMAP: LexMap =
- &[(Header::probe, |line| Token::Header(Header::lex(line)))];
-
-pub(in crate::syntax::content) fn read(
- text: &str,
-) -> String {
- parse(&lex(text, &make_lexmap::(LEXMAP)))
-}
-
-fn lex(text: &str, map: LexMap) -> Vec {
- let mut tokens: Vec = Vec::new();
-
- for raw_line in text.lines() {
- let line = Lexeme::Line(Line::new(raw_line));
-
- for &(ref probe, lex) in map {
- if probe(&line) {
- tokens.push(lex(&line));
- break;
- }
- }
- }
-
- tokens
-}
-
-fn parse(tokens: &[Token]) -> String {
- tokens
- .iter()
- .map(Token::render)
- .collect::>()
- .join("\n")
-}
diff --git a/src/syntax/content/parsers/word.rs b/src/syntax/content/parsers/word.rs
deleted file mode 100644
index 36ff23a..0000000
--- a/src/syntax/content/parsers/word.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-pub mod parser;
-pub mod elements;
-
-#[derive(Clone)]
-pub struct Word {
- pub raw: String,
-}
-
-impl Word {
- pub fn new(text: &str) -> Word {
- Word {
- raw: text.to_owned(),
- }
- }
-}
diff --git a/src/syntax/content/parsers/word/elements.rs b/src/syntax/content/parsers/word/elements.rs
deleted file mode 100644
index ff86880..0000000
--- a/src/syntax/content/parsers/word/elements.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub mod literal;
-pub mod anchor;
diff --git a/src/syntax/content/parsers/word/elements/anchor.rs b/src/syntax/content/parsers/word/elements/anchor.rs
deleted file mode 100644
index 8d0dc6a..0000000
--- a/src/syntax/content/parsers/word/elements/anchor.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-// use std::fmt::Display;
-// use crate::syntax::content::{Parseable, Line};
-//
-// pub struct Anchor {
-// text: String,
-// destination: String,
-// }
-//
-// impl Parseable for Anchor {
-// fn probe(line: &Line) -> bool {
-// let candidate = line.raw.split(' ');
-// !line.first.trim().is_empty()
-// && line.first.replace("#", "").is_empty()
-// && line.first.len() <= 6
-// }
-//
-// fn lex(line: &Line) -> Self {
-// Self {
-// text: line.raw.trim().to_owned(),
-// destination: t
-// }
-// }
-//
-// fn render(&self) -> String {
-// format!(r#"{}"#, &self.destination, &self.text)
-// }
-// }
-//
-// impl Display for Anchor {
-// fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-// write!(f, "Anchor: <{}>", &self.text)
-// }
-// }
diff --git a/src/syntax/content/parsers/word/parser.rs b/src/syntax/content/parsers/word/parser.rs
deleted file mode 100644
index 19a3688..0000000
--- a/src/syntax/content/parsers/word/parser.rs
+++ /dev/null
@@ -1,37 +0,0 @@
-use crate::syntax::content::parsers::word::elements::literal::Literal;
-use crate::syntax::content::{Parseable, Token, Word, LexMap, make_lexmap};
-use crate::syntax::content::lexeme::Lexeme;
-
-const LEXMAP: LexMap =
- &[(Literal::probe, |line| Token::Literal(Literal::lex(line)))];
-
-pub(in crate::syntax::content) fn read(
- text: &str,
-) -> String {
- parse(&lex(text, &make_lexmap::(LEXMAP)))
-}
-
-fn lex(text: &str, map: LexMap) -> Vec {
- let mut tokens: Vec = Vec::new();
-
- for raw_word in text.split(" ") {
- let word = Lexeme::Word(Word::new(raw_word));
-
- for &(ref probe, lex) in map {
- if probe(&word) {
- tokens.push(lex(&word));
- break;
- }
- }
- }
-
- tokens
-}
-
-fn parse(tokens: &[Token]) -> String {
- tokens
- .iter()
- .map(Token::render)
- .collect::>()
- .join(" ")
-}
diff --git a/src/syntax/content/token.rs b/src/syntax/content/token.rs
deleted file mode 100644
index 5871fb6..0000000
--- a/src/syntax/content/token.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-use super::Parseable as _;
-use super::parsers::word::elements::{literal::Literal};
-use super::parsers::line::elements::{
- paragraph::Paragraph, header::Header, span::Span,
-};
-
-pub enum Token {
- Paragraph(Paragraph),
- Header(Header),
- Span(Span),
- Literal(Literal),
-}
-
-impl Token {
- pub fn render(&self) -> String {
- match *self {
- Token::Paragraph(ref d) => d.render(),
- Token::Header(ref d) => d.render(),
- Token::Span(ref d) => d.render(),
- Token::Literal(ref d) => d.render(),
- }
- }
-}
-
-impl From for Token {
- fn from(d: Paragraph) -> Token {
- Token::Paragraph(d)
- }
-}
-
-impl From for Token {
- fn from(d: Header) -> Token {
- Token::Header(d)
- }
-}
-
-impl From for Token {
- fn from(d: Span) -> Token {
- Token::Span(d)
- }
-}
-
-impl From for Token {
- fn from(d: Literal) -> Token {
- Token::Literal(d)
- }
-}
diff --git a/src/types.rs b/src/types.rs
index 5c8ea95..85c2a89 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -2,7 +2,7 @@ use std::collections::HashMap;
use serde::{Serialize, Deserialize};
-use crate::syntax::content::parsers::{compound::elements::literal::Literal, line::elements::{paragraph::Paragraph, span::Span}};
+use crate::syntax::content;
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub struct Graph {
@@ -155,14 +155,9 @@ impl Node {
impl Config {
#[must_use]
pub fn parse_text(self) -> Config {
-
Config {
- footer_text: crate::syntax::content::parse::(
- &self.footer_text,
- ),
- about_text: crate::syntax::content::parse::(
- &self.about_text,
- ),
+ footer_text: content::parse(&self.footer_text),
+ about_text: content::parse(&self.about_text),
..self
}
}
diff --git a/static/graph.toml b/static/graph.toml
index f2ec600..f8534b8 100644
--- a/static/graph.toml
+++ b/static/graph.toml
@@ -7,11 +7,13 @@ text = """
For now, if you want to try en, you must build it yourself.
-In an environment with a Rust toolchain and Git installed, run:
+In an environment with a |Rust toolchain|https://rustup.rs/ and Git installed, run:
+`
git clone https://codeberg.org/jutty/en
cd en
cargo build --release
+`
The en binary will be in target/release/en.
@@ -19,32 +21,40 @@ The en binary will be in target/release/en.
The graph is a TOML file. You can create nodes by adding text such as:
+`
[nodes.Computer]
text = "A computer is a machine capable of executing arbitrary instructions."
+`
If you need longer text, it's more convenient to use triple-quoted syntax:
+`
[nodes.Computer]
text = \"""
A computer is a machine capable of executing arbitrary instructions.
\"""
+`
Nodes can have connections between each other.
To add a simple connection without any associated properties, you can simply add links:
+`
[nodes.Quark]
text = "A subatomic particle that forms hadrons."
links = [ "Particle", "Hadron" ]
+`
This will create two outgoing connections from Quark: to Particle and to Hadron. It will also list Quark as an incoming connection in these nodes' pages.
If you want to add properties to the connection, you can use the connection syntax:
+`
[[nodes.Quark.connections]]
to = "Particle physics"
anchor = "particle"
+`
This will create a connection from Quark to "Particle physics", and the first occurrence of the word "particle" in the text of Quark gets anchored to this connection.
@@ -54,30 +64,38 @@ You can set the hostname, port and graph file path using CLI options:
For the hostname, use -h or --hostname:
+`
en -h localhost
en --hostname 10.120.0.5
+`
If unspecified, the default is 0.0.0.0.
For the port, use -p or --port:
+`
en -p 3003
en --port 3000
+`
If unspecified, the default is to use a random available port assigned by the operating system.
For the graph path, use -g or --graph:
+`
en -g graph.toml
en --g ./static/my-graph.toml
+`
If unspecified, the default is ./static/graph.toml.
You can combine these options as you wish:
+`
en -h localhost -p 3000
en -p 3003 --host localhost --graph ./graph.toml
en --g ./graph.toml -p 1312
+`
If an option is specified more than once, the last use will override any previous ones.
@@ -127,16 +145,16 @@ To see the TOML declaration that translates into the rendered graph you are read
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/)
+- |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
+made by jutty|https://jutty.dev • acknowledgements|Acknowledgments • |source code|https://codeberg.org/jutty/en
"""
diff --git a/static/style.css b/static/style.css
index 55babcf..ef3d541 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1,7 +1,3 @@
-* {
- line-height: 1.6em;
-}
-
html {
height: 100%;
}