Implement blockquote token

This commit is contained in:
Juno Takano 2026-02-08 17:08:16 -03:00
commit 260610c4a0
11 changed files with 263 additions and 120 deletions

View file

@ -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<Token>) {
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)));

View file

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

View file

@ -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() {

View file

@ -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<Token>,
iterator: &mut Peekable<Iter<'_, Lexeme>>,
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
}

View file

@ -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<String, Vec<String>>,
@ -15,7 +15,7 @@ pub struct State {
pub format_tokens: Vec<Token>,
}
#[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::*;

View file

@ -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<bool>,
pub text: String,
pub citation: Option<String>,
pub url: Option<String>,
}
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 {
"<blockquote>".to_owned()
} else {
"</blockquote>".to_owned()
}
let opening = if let Some(url) = &self.url {
format!(r#"<blockquote cite="{url}">"#)
} else {
panic!("Attempt to render a quote tag while open state is unknown")
}
String::from("<blockquote>")
};
let content = if let Some(citation) = &self.citation {
format!(
r#"{}<br/><p class="quote-citation">{citation}</p>"#,
&self.text
)
} else {
String::from(&self.text)
};
format!("\n{opening}\n{content}\n</blockquote>\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())
}
}