From 3837af387a7c03d688f17eae5cec10f2f54e3f45 Mon Sep 17 00:00:00 2001 From: jutty Date: Sat, 10 Jan 2026 01:18:47 -0300 Subject: [PATCH] Implement nested formatting --- Cargo.toml | 1 - src/syntax/content/parser.rs | 30 +++++++----- src/syntax/content/parser/context/block.rs | 6 ++- src/syntax/content/parser/context/list.rs | 5 ++ src/syntax/content/parser/point.rs | 14 ++++-- src/syntax/content/parser/token/list.rs | 56 +++++++++++----------- static/graph.toml | 21 ++++---- 7 files changed, 77 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89f471f..d21f591 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,6 @@ explicit_iter_loop = "warn" filter_map_next = "warn" flat_map_option = "warn" float_cmp = "warn" -format_push_string = "warn" from_iter_instead_of_collect = "warn" if_not_else = "warn" ignore_without_reason = "warn" diff --git a/src/syntax/content/parser.rs b/src/syntax/content/parser.rs index 491b292..18ee600 100644 --- a/src/syntax/content/parser.rs +++ b/src/syntax/content/parser.rs @@ -20,7 +20,7 @@ const LEXMAP: LexMap = &[ }), ]; -fn lex(text: &str, map: LexMap, config: &Config) -> Vec { +fn lex(text: &str, map: LexMap, config: &Config, blocking: bool) -> Vec { let mut tokens: Vec = Vec::default(); let mut state = state::State::default(); @@ -38,14 +38,16 @@ fn lex(text: &str, map: LexMap, config: &Config) -> Vec { continue; } - if context::block::parse( - lexeme, - &mut state, - &mut tokens, - &mut iterator, - config, - ) { - continue; + if blocking { + if context::block::parse( + lexeme, + &mut state, + &mut tokens, + &mut iterator, + config, + ) { + continue; + } } if point::parse(lexeme, &mut state, &mut tokens, &mut iterator) { @@ -79,8 +81,14 @@ fn parse(tokens: &[Token]) -> String { tokens.iter().map(Token::render).collect::() } -pub(super) fn read(text: &str, config: &Config) -> String { - parse(&lex(text, LEXMAP, config)) +/// Apply end-to-end point and inline parsing for nested contexts, such as +/// inside the displayed text of other tokens like anchors and list items +pub fn nest(input: &str, config: &Config) -> String { + parse(&lex(input, LEXMAP, config, false)) +} + +pub(super) fn read(input: &str, config: &Config) -> String { + parse(&lex(input, LEXMAP, config, true)) } #[cfg(test)] diff --git a/src/syntax/content/parser/context/block.rs b/src/syntax/content/parser/context/block.rs index b1fad4c..e2d278e 100644 --- a/src/syntax/content/parser/context/block.rs +++ b/src/syntax/content/parser/context/block.rs @@ -46,7 +46,9 @@ pub fn parse( log!("Block Context: None -> List on {lexeme}"); state.context.block = Block::List; state.buffers.list.candidate.ordered = lexeme.match_char('+'); - return super::list::parse(lexeme, state, tokens, iterator); + return super::list::parse( + lexeme, state, tokens, iterator, config, + ); } else if Paragraph::probe(lexeme) { log!("Block Context: None -> Paragraph on {lexeme}"); state.context.block = Block::Paragraph; @@ -78,7 +80,7 @@ pub fn parse( } }, Block::List => { - return super::list::parse(lexeme, state, tokens, iterator); + return super::list::parse(lexeme, state, tokens, iterator, config); }, } false diff --git a/src/syntax/content/parser/context/list.rs b/src/syntax/content/parser/context/list.rs index e44b7f2..0b70fb7 100644 --- a/src/syntax/content/parser/context/list.rs +++ b/src/syntax/content/parser/context/list.rs @@ -5,9 +5,11 @@ use crate::{ syntax::content::parser::{ context::Block, lexeme::Lexeme, + nest, state::{ListBuffer, State}, token::{Token, item::Item}, }, + types::Config, }; /// Handles open list contexts until a list is fully parsed. @@ -23,6 +25,7 @@ pub fn parse( state: &mut State, tokens: &mut Vec, iterator: &mut Peekable>, + config: &Config, ) -> bool { let buffer = &mut state.buffers.list; let candidate = &mut buffer.candidate; @@ -49,6 +52,7 @@ pub fn parse( } if item_candidate.depth.is_some() { // if the current item candidate has a known depth, push it + item_candidate.text = nest(&item_candidate.text, config); candidate.items.push(item_candidate.clone()); } // push list candidate, reset state and exit context @@ -60,6 +64,7 @@ pub fn parse( } else if lexeme.match_char('\n') { // found end of item, push it and reset state log!("Accepting item candidate {item_candidate}"); + item_candidate.text = nest(&item_candidate.text, config); candidate.items.push(item_candidate.clone()); *item_candidate = Item::default(); buffer.depth = 0; diff --git a/src/syntax/content/parser/point.rs b/src/syntax/content/parser/point.rs index db3d69c..9d9b10d 100644 --- a/src/syntax/content/parser/point.rs +++ b/src/syntax/content/parser/point.rs @@ -6,11 +6,11 @@ use crate::{ Parseable as _, parser::{ lexeme::Lexeme, - token::{ - Token, oblique::Oblique, bold::Bold, underline::Underline, - strike::Strike, - }, state::State, + token::{ + Token, bold::Bold, checkbox::CheckBox, oblique::Oblique, + strike::Strike, underline::Underline, + }, }, }, }; @@ -50,6 +50,12 @@ pub fn parse( tokens.push(Token::Bold(Bold::new(!state.switches.bold))); state.switches.bold = !state.switches.bold; return true; + } else if CheckBox::probe(lexeme) { + log!("CheckBox probed: {lexeme}"); + tokens.push(Token::CheckBox(CheckBox::lex(lexeme))); + iterator.next(); + iterator.next(); + return true; } false } diff --git a/src/syntax/content/parser/token/list.rs b/src/syntax/content/parser/token/list.rs index dcb2bc7..7130282 100644 --- a/src/syntax/content/parser/token/list.rs +++ b/src/syntax/content/parser/token/list.rs @@ -1,7 +1,4 @@ -use std::fmt::Write as _; - use crate::{ - prelude::*, syntax::content::{Lexeme, Parseable, parser::token::item::Item}, }; @@ -23,41 +20,31 @@ impl Parseable for List { fn render(&self) -> String { let tag = if self.ordered { "ol" } else { "ul" }; let mut output = String::new(); - - let indent_width = self - .items - .windows(2) - .find_map(|pair| { - let a = pair[0].depth?; - let b = pair[1].depth?; - (b > a).then_some(b - a) - }) - .unwrap_or(1); + let scale = self.scale_indent(); let mut iterator = self.items.iter().peekable(); - while let Some(item) = iterator.next() { - let current_level = item.depth.unwrap_or(0) / indent_width; - let next_level = iterator.peek().and_then(|n| n.depth).unwrap_or(0) - / indent_width; + let level = item.depth.unwrap_or(0).strict_div(scale); + let next_level = iterator + .peek() + .and_then(|n| n.depth) + .unwrap_or(0) + .strict_div(scale); - output.push_str("
  • "); - output.push_str(&item.text); + output.push_str(&format!("
  • {}", item.text)); - if next_level > current_level { - // Open nested list(s), keep
  • open - for _ in 0..(next_level - current_level) { + if next_level > level { + // open nested lists + for _ in 0..(next_level.saturating_sub(level)) { output.push_str(&format!("<{tag}>\n")); } } else { - // close current
  • + // close current item output.push_str("
  • "); - - // close nested lists inline - for _ in 0..(current_level - next_level) { + // close nested lists + for _ in 0..(level.saturating_sub(next_level)) { output.push_str(&format!("")); } - output.push('\n'); } } @@ -73,6 +60,21 @@ impl List { items: vec![], } } + + fn scale_indent(&self) -> u8 { + let width = self + .items + .windows(2) + .find_map(|pair| { + let outer = pair.first()?.depth?; + let inner = pair.get(1)?.depth?; + (inner > outer).then_some(inner.saturating_sub(outer)) + }) + .unwrap_or(1); + + assert!(width != 0, "Width of zero can't be a divisor"); + width + } } impl std::fmt::Display for List { diff --git a/static/graph.toml b/static/graph.toml index a8330f7..33c6f7f 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -516,8 +516,6 @@ en is only possible thanks to a number of projects and people: [nodes.Roadmap] text = """ -- [x] Setup tests - - [ ] Improve content syntax parser coverage - [x] Redirects - [ ] Strip/render some syntax in Tree text preview - [x] Drop-down navigation @@ -557,19 +555,19 @@ text = """ - [ ] Specialization <-> Generalization - [ ] Custom connection kinds - [x] External anchors -- [ ] Richer text formatting - - [ ] Nested formatting +- [x] Richer text formatting + - [x] Nested formatting - [x] Headers - [x] Preformatted blocks - - [x] Oblique, - - [x] Underline - - [x] Strikethrough - - [x] Bold - - [x] Inline code + - [x] _Oblique_, + - [x] __Underline__ + - [x] ~~Strikethrough~~ + - [x] *Bold* + - [x] `Inline code` - [x] Lists - - [ ] Nested lists + - [x] Nested lists - [x] Checkboxes - - [ ] Move this roadmap to en + - [x] Move this roadmap to en - [ ] Full-text search - [ ] Begin centralizing state - [ ] Reduce O(n) calls in the formats module @@ -577,6 +575,7 @@ text = """ - [ ] Render to filesystem - [ ] Single-page rendering - [ ] Input formats + - [ ] Frontmatter format - [ ] Multi-file graphs - [ ] Multi-graph - [ ] Themes