Add word-level parsing

This commit is contained in:
Juno Takano 2025-12-18 02:20:11 -03:00
commit 198bc12507
34 changed files with 743 additions and 446 deletions

View file

@ -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

View file

@ -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<Body> {
let content = match std::fs::read(file_path) {
Ok(s) => s,

View file

@ -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<String>) -> Response<Body> {
@ -17,7 +15,7 @@ pub async fn node(Path(id): Path<String>) -> Response<Body> {
context.insert("incoming", &graph.incoming.get(&id));
context.insert("config", &graph.meta.config.parse_text());
let out_text = content::parse::<Paragraph, Literal>(&node.text);
let out_text = content::parse(&node.text);
context.insert("text", &out_text);
let not_found = *node == empty_node;

View file

@ -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<Body> {

View file

@ -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#"<!DOCTYPE html>
format!(
r#"<!DOCTYPE html>
<html>
<head>
<title>Pre-Templating Error</title>
@ -105,5 +106,6 @@ fn emergency_wrap(message: &tera::Error) -> String {
</p>
</body>
</html>
"#)
"#
)
}

View file

@ -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")

View file

@ -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<Token> {
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<DefaultToken: Parseable>(base: LexMap) -> Vec<(Probe, Lexer)> {
let mut vector: Vec<(Probe, Lexer)> = base.to_vec();
fn adapter<D: Parseable>(lex: &Lexeme) -> Token {
D::lex(lex).into()
}
vector.push((DefaultToken::probe, adapter::<DefaultToken>));
vector
}
pub fn parse<DefaultLineToken: Parseable, DefaultWordToken: Parseable>(
text: &str,
) -> String {
let escaped_text = tera::escape_html(text);
parsers::line::parser::read::<DefaultLineToken>(
&parsers::word::parser::read::<DefaultWordToken>(&escaped_text),
)
pub fn parse(text: &str) -> String {
parser::read(text)
}

View file

@ -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<String> {
self.to_raw().split(' ').map(str::to_string).collect()
}
pub fn first(self) -> Option<String> {
self.to_vec().first().map(String::to_owned)
}
}

View file

@ -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<Token> {
let mut tokens: Vec<Token> = 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<String> {
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<Item = &'i str>,
{
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("<a")
}
log!("On {current}[?]{next}");
if is_inline(next) {
log!("Pushing space because {next} is inline");
false
} else if is_closing(next) {
log!("Not pushing space because {next} is closing");
true
} else if is_opening(current) {
log!("Not pushing space because {current} is opening");
true
} else {
false
}
}
let mut iterator = rendered_tokens.into_iter();
let mut out_string = String::new();
if let Some(mut current) = iterator.next() {
out_string.push_str(current);
for next in iterator {
if stick(current, next) {
out_string.push_str(next);
} else {
out_string.push(' ');
out_string.push_str(next);
}
current = next;
}
}
out_string
}
fn parse(tokens: &[Token]) -> String {
let rendered: Vec<String> = 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))
}

View file

@ -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<char> {
let vector: Vec<char> = self.to_raw().chars().collect();
vector
}
pub fn split_words(self) -> Vec<String> {
self.to_raw().split(' ').map(str::to_string).collect()
}
pub fn first(self) -> Option<String> {
self.split_words().first().map(String::to_owned)
}
}

View file

@ -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(),
}
}
}

View file

@ -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<paragraph::Paragraph> for Token {
fn from(d: paragraph::Paragraph) -> Token {
Token::Paragraph(d)
}
}
impl From<header::Header> for Token {
fn from(d: header::Header) -> Token {
Token::Header(d)
}
}
impl From<span::Span> for Token {
fn from(d: span::Span) -> Token {
Token::Span(d)
}
}
impl From<literal::Literal> for Token {
fn from(d: literal::Literal) -> Token {
Token::Literal(d)
}
}
impl From<anchor::Anchor> for Token {
fn from(d: anchor::Anchor) -> Token {
Token::Anchor(d)
}
}
impl From<linebreak::LineBreak> for Token {
fn from(d: linebreak::LineBreak) -> Token {
Token::LineBreak(d)
}
}
impl From<preformat::PreFormat> for Token {
fn from(d: preformat::PreFormat) -> Token {
Token::PreFormat(d)
}
}

View file

@ -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<String> = 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#"<a href="{}">{}</a>"#, &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)
}
}

View file

@ -0,0 +1,137 @@
use crate::{
prelude::*,
syntax::content::{Parseable, Lexeme},
};
use std::fmt::Display;
pub struct Header {
open: Option<bool>,
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!("<h{}>", &self.level)
} else {
format!("</h{}>", &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<usize> 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"),
}
}
}

View file

@ -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")
}
}

View file

@ -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(),
}
}

View file

@ -0,0 +1,53 @@
use std::fmt::Display;
use crate::syntax::content::{Parseable, parser::lexeme::Lexeme};
pub struct Paragraph {
open: Option<bool>,
}
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 {
"<p>".to_owned()
} else {
"</p>".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)")
}
}
}

View file

@ -0,0 +1,43 @@
use crate::{
syntax::content::{Parseable, Lexeme},
};
pub struct PreFormat {
open: Option<bool>,
}
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 {
"<pre>".to_owned()
} else {
"</pre>".to_owned()
}
} else {
panic!(
"Attempt to render a preformat tag while open state is unknown"
)
}
}
}

View file

@ -0,0 +1,49 @@
use std::fmt::Display;
use crate::syntax::content::{Parseable, parser::lexeme::Lexeme};
pub struct Span {
open: Option<bool>,
}
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 {
"<span>".to_owned()
} else {
"</span>".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)")
}
}
}

View file

@ -1,2 +0,0 @@
pub mod line;
pub mod word;

View file

@ -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<Lexeme> 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,
}
}
}

View file

@ -1,3 +0,0 @@
pub mod header;
pub mod paragraph;
pub mod span;

View file

@ -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!("<h{}>{}</h{0}>", &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)
}
}

View file

@ -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!("<p>{}</p>", &self.text)
}
}
impl Display for Paragraph {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Paragraph: <{}>", &self.text)
}
}

View file

@ -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!("<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

@ -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<DefaultToken: Parseable>(
text: &str,
) -> String {
parse(&lex(text, &make_lexmap::<DefaultToken>(LEXMAP)))
}
fn lex(text: &str, map: LexMap) -> Vec<Token> {
let mut tokens: Vec<Token> = 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::<Vec<_>>()
.join("\n")
}

View file

@ -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(),
}
}
}

View file

@ -1,2 +0,0 @@
pub mod literal;
pub mod anchor;

View file

@ -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#"<a href="{}">{}</a>"#, &self.destination, &self.text)
// }
// }
//
// impl Display for Anchor {
// fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// write!(f, "Anchor: <{}>", &self.text)
// }
// }

View file

@ -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<DefaultToken: Parseable>(
text: &str,
) -> String {
parse(&lex(text, &make_lexmap::<DefaultToken>(LEXMAP)))
}
fn lex(text: &str, map: LexMap) -> Vec<Token> {
let mut tokens: Vec<Token> = 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::<Vec<_>>()
.join(" ")
}

View file

@ -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<Paragraph> for Token {
fn from(d: Paragraph) -> Token {
Token::Paragraph(d)
}
}
impl From<Header> for Token {
fn from(d: Header) -> Token {
Token::Header(d)
}
}
impl From<Span> for Token {
fn from(d: Span) -> Token {
Token::Span(d)
}
}
impl From<Literal> for Token {
fn from(d: Literal) -> Token {
Token::Literal(d)
}
}

View file

@ -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::<Span, Literal>(
&self.footer_text,
),
about_text: crate::syntax::content::parse::<Paragraph, Literal>(
&self.about_text,
),
footer_text: content::parse(&self.footer_text),
about_text: content::parse(&self.about_text),
..self
}
}

View file

@ -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
"""

View file

@ -1,7 +1,3 @@
* {
line-height: 1.6em;
}
html {
height: 100%;
}