Implement nested formatting

This commit is contained in:
Juno Takano 2026-01-10 01:18:47 -03:00
commit 3837af387a
7 changed files with 78 additions and 57 deletions

View file

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

View file

@ -20,7 +20,7 @@ const LEXMAP: LexMap = &[
}),
];
fn lex(text: &str, map: LexMap, config: &Config) -> Vec<Token> {
fn lex(text: &str, map: LexMap, config: &Config, blocking: bool) -> Vec<Token> {
let mut tokens: Vec<Token> = Vec::default();
let mut state = state::State::default();
@ -38,14 +38,16 @@ fn lex(text: &str, map: LexMap, config: &Config) -> Vec<Token> {
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::<String>()
}
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)]

View file

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

View file

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

View file

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

View file

@ -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("<li>");
output.push_str(&item.text);
output.push_str(&format!("<li>{}", item.text));
if next_level > current_level {
// Open nested list(s), keep <li> 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 <li>
// close current item
output.push_str("</li>");
// 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!("</{tag}></li>"));
}
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 {

View file

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