diff --git a/.justfile b/.justfile index ad3be8c..16db5d1 100644 --- a/.justfile +++ b/.justfile @@ -18,7 +18,7 @@ alias r := run # Build and serve on changes [group: 'develop'] run-watch: - {{ watch_cmd }} {{ just_cmd }} run + @{{ watch_cmd }} {{ just_cmd }} run alias w := run-watch diff --git a/Cargo.toml b/Cargo.toml index a002498..f0e85d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ doc_broken_link = "warn" doc_comment_double_space_linebreaks = "warn" doc_link_with_quotes = "warn" doc_markdown = "warn" -empty_enum = "warn" +empty_enums = "warn" expl_impl_clone_on_copy = "warn" explicit_deref_methods = "warn" explicit_into_iter_loop = "warn" diff --git a/src/syntax/content/parser/context.rs b/src/syntax/content/parser/context.rs index 7174ecf..094ffae 100644 --- a/src/syntax/content/parser/context.rs +++ b/src/syntax/content/parser/context.rs @@ -1,20 +1,21 @@ use crate::syntax::content::parser::{ State, Token, - token::{Header, Paragraph, PreFormat, Quote, Verse}, + token::{Header, Paragraph, PreFormat, Verse}, }; pub mod block; pub mod inline; pub mod anchor; pub mod list; +pub mod quote; -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub struct Context { pub block: Block, pub inline: Inline, } -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub enum Block { Paragraph, Header(u8), // level @@ -22,13 +23,15 @@ pub enum Block { PreFormat, Quote, Verse, + #[default] None, } -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub enum Inline { Anchor, Code, + #[default] None, } @@ -49,7 +52,7 @@ pub fn close(state: &State, tokens: &mut Vec) { tokens.push(Token::Header(Header::from_u8(level, false, None))); }, Block::Quote => { - tokens.push(Token::Quote(Quote::new(false))); + panic!("End of input with open quote") }, Block::Verse => { tokens.push(Token::Verse(Verse::new(false))); diff --git a/src/syntax/content/parser/context/block.rs b/src/syntax/content/parser/context/block.rs index 64b080d..16711bd 100644 --- a/src/syntax/content/parser/context/block.rs +++ b/src/syntax/content/parser/context/block.rs @@ -50,7 +50,7 @@ pub fn parse( } else if Quote::probe(lexeme) { log!(VERBOSE, "Block Context: None -> Quote on {lexeme}"); state.context.block = Block::Quote; - tokens.push(Token::Quote(Quote::new(true))); + iterator.next(); return true; } else if Verse::probe(lexeme) { log!(VERBOSE, "Block Context: None -> Verse on {lexeme}"); @@ -93,15 +93,7 @@ pub fn parse( return super::list::parse(lexeme, state, tokens, iterator, graph); }, Block::Quote => { - if lexeme.match_char_sequence('\n', '>') { - tokens.push(Token::LineBreak(LineBreak::default())); - iterator.next(); - return true; - } else if Quote::probe_end(lexeme) { - tokens.push(Token::Quote(Quote::new(false))); - log!(VERBOSE, "Block Context: Quote -> None on {lexeme}"); - state.context.block = Block::None; - } + return super::quote::parse(lexeme, state, tokens, iterator, graph); }, Block::Verse => { if Verse::probe_end(lexeme) { diff --git a/src/syntax/content/parser/context/list.rs b/src/syntax/content/parser/context/list.rs index bec908d..1fc938a 100644 --- a/src/syntax/content/parser/context/list.rs +++ b/src/syntax/content/parser/context/list.rs @@ -30,6 +30,7 @@ pub fn parse( let candidate = &mut buffer.candidate; let item_candidate = &mut buffer.item_candidate; + #[allow(clippy::wildcard_enum_match_arm)] match state.context.block { Block::List => { if lexeme.match_char(' ') && item_candidate.depth.is_none() { diff --git a/src/syntax/content/parser/context/quote.rs b/src/syntax/content/parser/context/quote.rs new file mode 100644 index 0000000..f09a8f7 --- /dev/null +++ b/src/syntax/content/parser/context/quote.rs @@ -0,0 +1,97 @@ +use std::{iter::Peekable, slice::Iter}; + +use crate::{ + graph::Graph, + prelude::*, + syntax::content::parser::{ + Lexeme, State, Token, + context::Block, + format, state, + token::{Anchor, Quote}, + }, +}; + +/// Handles open quote contexts until a quote is fully parsed. +/// +/// A return of `true` will trigger a continue in the outer parser, +/// skipping any further parsing of the current lexeme. +/// +/// # Panics +/// This parser can handle only the Quote context, and will panic if passed an +/// unrelated context since it has no knowledge on how to handle them. +pub fn parse( + lexeme: &Lexeme, + state: &mut State, + tokens: &mut Vec, + iterator: &mut Peekable>, + graph: &Graph, +) -> bool { + let buffer = &mut state.buffers.quote; + let candidate = &mut buffer.candidate; + + #[allow(clippy::wildcard_enum_match_arm)] + match state.context.block { + Block::Quote => { + if Quote::probe_end(lexeme) { + log!("Probed end of quote on {lexeme}"); + let (text, text_tokens) = format(&candidate.text, graph); + candidate.text = text; + state.format_tokens.extend_from_slice(&text_tokens); + + if let Some(citation) = &candidate.citation { + let (formatted_citation, citation_tokens) = + format(citation, graph); + candidate.citation = Some(formatted_citation); + state.format_tokens.extend_from_slice(&citation_tokens); + + let mut first_anchor = Anchor::default(); + for token in citation_tokens { + if let Token::Anchor(token_data) = token { + first_anchor = *token_data.clone(); + break; + } + } + if first_anchor.external() { + candidate.url = first_anchor.destination(); + } + } + + tokens.push(Token::Quote(candidate.clone())); + log!(VERBOSE, "Block Context: Quote -> None on {lexeme}"); + state.context.block = Block::None; + *buffer = state::QuoteBuffer::default(); + } else if !buffer.in_citation + && lexeme.match_char('\n') + && lexeme.next() == "--" + { + log!("Matched citation start on {lexeme}"); + buffer.in_citation = true; + iterator.next(); + iterator.next(); + } else if lexeme.match_char_sequence('\n', '>') { + log!("Matched break-aware sequence on {lexeme}"); + candidate.text.push_str(" <\n"); + iterator.next(); + } else { + log!("Entered quote else branch on {lexeme}"); + if buffer.in_citation { + log!("Extending citation on {lexeme}"); + candidate.extend_citation(&lexeme.text()); + if lexeme.match_char('\n') && lexeme.next() == "--" { + candidate.text.push('\n'); + iterator.next(); + } else if lexeme.match_char('\n') { + buffer.in_citation = false; + } + } else { + log!("Extending quote on {lexeme}"); + candidate.text.push_str(&lexeme.text()); + } + } + }, + _ => { + panic!("Quote context parser called to handle non-quote context") + }, + } + true +} diff --git a/src/syntax/content/parser/state.rs b/src/syntax/content/parser/state.rs index 851b0e7..0059548 100644 --- a/src/syntax/content/parser/state.rs +++ b/src/syntax/content/parser/state.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use crate::syntax::content::parser::{ Token, - context::{Block, Context, Inline}, - token::{Anchor, Item, List}, + context::Context, + token::{Anchor, Item, List, Quote}, }; -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub struct State { pub context: Context, pub dom_ids: HashMap>, @@ -15,7 +15,7 @@ pub struct State { pub format_tokens: Vec, } -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub struct Switches { pub bold: bool, pub oblique: bool, @@ -23,10 +23,11 @@ pub struct Switches { pub underline: bool, } -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub struct Buffers { pub anchor: AnchorBuffer, pub list: ListBuffer, + pub quote: QuoteBuffer, } #[derive(Default, Clone, Debug)] @@ -43,6 +44,12 @@ pub struct AnchorBuffer { pub destination: String, } +#[derive(Default, Clone, Debug)] +pub struct QuoteBuffer { + pub candidate: Quote, + pub in_citation: bool, +} + impl std::fmt::Display for AnchorBuffer { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let display_text = if self.text.is_empty() { @@ -67,37 +74,6 @@ impl std::fmt::Display for AnchorBuffer { } } -impl Default for State { - fn default() -> State { - State { - context: Context { - inline: Inline::None, - block: Block::None, - }, - dom_ids: HashMap::default(), - switches: Switches { - bold: false, - crossout: false, - oblique: false, - underline: false, - }, - buffers: Buffers { - anchor: AnchorBuffer { - candidate: Anchor::default(), - text: String::default(), - destination: String::default(), - }, - list: ListBuffer { - candidate: List::default(), - item_candidate: Item::default(), - depth: 0, - }, - }, - format_tokens: vec![], - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/syntax/content/parser/token/quote.rs b/src/syntax/content/parser/token/quote.rs index e9bdf49..325654e 100644 --- a/src/syntax/content/parser/token/quote.rs +++ b/src/syntax/content/parser/token/quote.rs @@ -1,18 +1,24 @@ use crate::syntax::content::{Parseable, parser::Lexeme}; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Default, Clone, Eq, PartialEq)] pub struct Quote { - open: Option, + pub text: String, + pub citation: Option, + pub url: Option, } impl Quote { - pub fn new(open: bool) -> Quote { - Quote { open: Some(open) } - } - pub fn probe_end(lexeme: &Lexeme) -> bool { lexeme.match_char_sequence('\n', '\n') } + + pub fn extend_citation(&mut self, s: &str) { + if let Some(current) = &self.citation { + self.citation = Some(format!("{current}{s}")); + } else { + self.citation = Some(String::from(s)); + } + } } impl Parseable for Quote { @@ -21,19 +27,26 @@ impl Parseable for Quote { } fn lex(_lexeme: &Lexeme) -> Quote { - Quote { open: None } + Quote::default() } fn render(&self) -> String { - if let Some(open) = self.open { - if open { - "
".to_owned() - } else { - "
".to_owned() - } + let opening = if let Some(url) = &self.url { + format!(r#"
"#) } else { - panic!("Attempt to render a quote tag while open state is unknown") - } + String::from("
") + }; + + let content = if let Some(citation) = &self.citation { + format!( + r#"{}

{citation}

"#, + &self.text + ) + } else { + String::from(&self.text) + }; + + format!("\n{opening}\n{content}\n
\n") } fn flatten(&self) -> String { @@ -43,16 +56,14 @@ impl Parseable for Quote { impl std::fmt::Display for Quote { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let display_open_state = match self.open { - Some(open_state) => { - if open_state { - "open" - } else { - "closed" - } - }, - None => "unknown", - }; - write!(f, "Quote [{display_open_state}]") + let mut meta = String::default(); + if self.url.is_some() { + meta.push_str("+url "); + } + if self.citation.is_some() { + meta.push_str("+citation "); + } + + write!(f, "Quote [{}]", meta.trim()) } } diff --git a/static/graph.toml b/static/graph.toml index cb33cf5..7882407 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -188,7 +188,7 @@ If you are familiar with Markdown|https://en.wikipedia.org/wiki/Markdown|, you'l ## Anchors -Anchors have the following basic syntax: +Anchors are the most important and powerful syntactic element you will work with because they can create connections between nodes when you use them. They have the following basic syntax: ` anchor|destination @@ -197,11 +197,18 @@ anchor|destination For example: ` -DRC|DemocraticRepublicOfTheCongo +particles|ParticlePhysics +` + +This example will render as the word "particles" pointing to a node with ID `ParticlePhysics` because the destination is not an external URL. + +An external anchor looks like this: + +` docs|https://en.jutty.dev/node/Documentation ` -As shown above, anchors can point to external addresses. These are identified by the presence of either a `:` or a `/` character in the destination. Otherwise, the anchor will point to a node with an ID matching the destination. This means your anchors to external URLs, special handlers such as `mailto:user@domain.com` and destinations relative to the website root like `/about` will all work as intended without being interpreted as node IDs. +External anchors are identified by the presence of either a `:` or a `/` character in the destination. This works for special handlers, such as `mailto:user@domain.com`, and destinations relative to the website root like `/about`. If the left side contains spaces, you need a leading `|` character: @@ -223,6 +230,8 @@ For internal anchors, most punctuation is automatically separated from the ancho This gem|PreciousStone, though green, was not an emerald. ` +> This gem|PreciousStone, though green, was not an emerald. + However, for external anchors, you want to add a third `|` to explicitly set the end because external URLs can have all sorts of arbitrary characters. ### Node anchors @@ -235,14 +244,6 @@ A node ID wrapped in two `|` characters will become an anchor to that node: |ParticlePhysics| ` -And two words separated by a single anchor allow you to set a display text and destination: - -` -particles|ParticlePhysics -` - -This example will render as "particles|ParticlePhysics": the word particles pointing to a node with id `ParticlePhysics`. - en can resolve IDs case insensitively (with priority to case-sensitive matches) and will also collapse spaces when trying to resolve an ID, so you can also write: ` @@ -285,6 +286,9 @@ You can use `[ ]` and `[x]` to render checkboxes: - [x] done ` +- [ ] not done +- [x] done + ## Blocks A block is any group of lines separated by empty lines: @@ -292,12 +296,12 @@ A block is any group of lines separated by empty lines: ` block A still block A -still block A +block A's last line block B starts here +block B ends here -block C starts here -still block C +this is block C ` By default, a block not starting with any special syntax is a paragraph, such as this very line you are reading. @@ -310,7 +314,7 @@ b c ` -You still get "a b c" as a result. This is the case for paragraphs and blockquotes, but not for lists, verse blocks and preformatted text. +You still get "a b c" as a result. This is the case for paragraphs, but not for lists, verse blocks and preformatted text. Blockquotes support both modes. This is useful when editing your text, allowing you to break some thoughts and special syntax without losing control over where your paragraph ends, particularly when handling huge paragraphs. @@ -332,7 +336,7 @@ While useful to break a few lines on demand, if you have a large block of lines ### Verse -Verse blocks are delimited by a `&` character at their first and last line and are useful to avoid precisely this line-joining behavior of most blocks. They will break all lines without need for a trailing `<` character: +Verse blocks are delimited by a `&` character at their first and last line and are useful to avoid precisely this line-joining behavior of paragraphs. They will break all lines without need for a trailing `<` character: ` & @@ -342,8 +346,6 @@ Verse blocks are delimited by a `&` character at their first and last line and a & ` -This will be rendered as: - & these lines break just fine @@ -352,37 +354,79 @@ This will be rendered as: ### Quotes -A block of lines starting with a `>` character create a quote: +A block of lines starting with a `>` character will render as a quote: ` -> this is a quote +> Who'll change old lamps for new ones? ` +> Who'll change old lamps for new ones? + Quote blocks have two forms. If you prepend all blocks with a `>`, line breaks will be preserved, not collapsing the whole quote into a single line: -> a quote where all lines start with > BR -> still inside BR -> still inside BR -> -> above was a line with just a > plus a break BR -> still inside BR -> next is an empty line BR +` +> When I was alive +> I was dust which was, +> But now I am dust in dust +> I am dust which never was. +` + +> When I was alive +> I was dust which was, +> But now I am dust in dust +> I am dust which never was. If you would like the quote to be collapsed into a single line instead, you can leave just the first `>` and continue until the next empty line: -> this quote starts here -and continues here -until an empty line is found +` +> Dois grandes mitos dominam a história oficial do Brasil: +o mito da índole pacífica do brasileiro e o da "democracia racial". +-- Benedita da Silva, Speech on the Federal Senate, March 3rd, 1995 +` + +> Dois grandes mitos dominam a história oficial do Brasil: +o mito da índole pacífica do brasileiro e o da "democracia racial". +-- Benedita da Silva, Speech on the Federal Senate, March 3rd, 1995 You can still use `<` characters to force line breaks in this case. #### Citation -To add a citation to your qutoe block, you can add lines starting with two `-` characters: +To add a citation to your quote block, start a line with two `-` characters: -> a quote with _nested_ formatting *syntax and in* particular an anchor|Roadmap BR -> the quote continuation as it goes on and on --- quote by |Person Johnson|https://personjohnson.com +` +> with no more communion +> to down as morning pick-me-ups +> to sweeten afternoon naps +> to soothe nightmares +-- Assotto Saint, The Language of Dust +` + +> with no more communion +> to down as morning pick-me-ups +> to sweeten afternoon naps +> to soothe nightmares +-- Assotto Saint, The Language of Dust + +If you have a more complex citation, you can use multiple lines starting with `--`. All such lines will be joined together in the citation. If you need to break lines, use the `<` character at the end of a line: + +` +> Dois grandes mitos dominam a história oficial do Brasil: +o mito da índole pacífica do brasileiro e o da "democracia racial". +-- Benedita da Silva, +-- |Speech on the Federal Senate|https://www25.senado.leg.br/web/atividade/pronunciamentos/-/p/pronunciamento/165765|, +-- March 3rd, 1995, < +-- _Dia Internacional para a Eliminação da Discriminação Racial._ +` + +> Dois grandes mitos dominam a história oficial do Brasil: +o mito da índole pacífica do brasileiro e o da "democracia racial". +-- Benedita da Silva, +-- |Speech on the Federal Senate|https://www25.senado.leg.br/web/atividade/pronunciamentos/-/p/pronunciamento/165765|, +-- March 3rd, 1995, < +-- Dia Internacional para a Eliminação da Discriminação Racial. + +The first URL found in your citation will be used as the blockquote element's `cite` field. ### Lists @@ -394,6 +438,10 @@ A block of lines starting with a `-` character will be rendered as an unordered - crimson ` +- cyan +- amber +- crimson + Lines starting with a `+` character will create numbered lists instead: ` @@ -402,6 +450,10 @@ Lines starting with a `+` character will create numbered lists instead: + san ` ++ ichi ++ ni ++ san + ## Rendering unformatted text The backtick character `\\`` can be used to render unformatted blocks and inline text: @@ -410,6 +462,8 @@ The backtick character `\\`` can be used to render unformatted blocks and inline The asterisk `*` is special in en markup syntax. ` +> The asterisk `*` is special in en markup syntax. + Using the syntax above, the asterisk won't be interpreted as the start of bold formatting and instead will be shown like this: `*`. This is useful for code but also for rendering characters with special meaning you wish to mention literally. @@ -437,8 +491,6 @@ If you need some more advanced feature that is not supported directly by en's ma </table> ` -Which will render to: - @@ -653,8 +705,8 @@ en is only possible thanks to a number of projects and people: - Neovim|https://neovim.io/ - foot|https://codeberg.org/dnkl/foot - tmux|https://github.com/tmux/tmux/ -- |Void Linux|https://voidlinux.org/ and its kernel|https://www.kernel.org/ -- LibreWolf|https://librewolf.net/ +- |Debian|https://debian.org/ and its kernel|https://www.kernel.org/ +- LibreWolf|https://librewolf.net/ and its upstream |Mozilla Firefox|https://www.firefox.com/ - InkScape|https://inkscape.org/ """ diff --git a/static/public/assets/style.css b/static/public/assets/style.css index 0ea8c33..73d9f19 100644 --- a/static/public/assets/style.css +++ b/static/public/assets/style.css @@ -25,7 +25,7 @@ main { box-sizing: border-box; } -p, section li { +p:not(.quote-citation), section li { font-family: prose, sans-serif; } @@ -337,12 +337,23 @@ blockquote { font-size: calc(var(--base-font-size) * 1.6); } -.verse { +p.verse { font-family: serifed; font-size: calc(var(--base-font-size) * 1.6); margin-left: 2em; } +.quote-citation { + margin: 0.5em 0 0 0; + text-indent: -1.2em; + padding-left: 1em; + font-size: calc(var(--base-font-size) * 0.8); +} + +.quote-citation::before { + content: "— "; +} + @media (prefers-color-scheme: dark) { * { background: #222222; diff --git a/templates/node.html b/templates/node.html index 0c686e4..6d19a8a 100644 --- a/templates/node.html +++ b/templates/node.html @@ -44,7 +44,7 @@ {% endif %} {% if incoming %}
-

Incoming

+

Incoming

    {% for connection in incoming %}
Hi,