Implement nested lists

This commit is contained in:
Juno Takano 2026-01-07 15:11:50 -03:00
commit e42c67676d
11 changed files with 475 additions and 131 deletions

View file

@ -16,7 +16,7 @@ impl CheckBox {
impl Parseable for CheckBox {
fn probe(lexeme: &Lexeme) -> bool {
lexeme.match_triple_as_char(('[', ' ', ']'))
|| lexeme.match_triple_as_char(('[', 'x', ']'))
|| lexeme.match_triple_as_char(('[', 'x', ']'))
}
fn lex(lexeme: &Lexeme) -> CheckBox {
@ -30,11 +30,7 @@ impl Parseable for CheckBox {
}
fn render(&self) -> String {
let toggle = if self.checked {
" checked "
} else {
""
};
let toggle = if self.checked { " checked " } else { "" };
format!(r#"<input type="checkbox"{toggle}/>"#)
}
}

View file

@ -232,16 +232,25 @@ mod tests {
fn id_deduplication() {
let mut map: HashMap<String, Vec<String>> = HashMap::default();
let config = Config::default();
let id =
Header::make_id(&config, &Lexeme::new("##", "UVrcCUjoQ", ""), &mut map);
let id = Header::make_id(
&config,
&Lexeme::new("##", "UVrcCUjoQ", ""),
&mut map,
);
assert_eq!(id, "UVrcCUjoQ");
let double =
Header::make_id(&config, &Lexeme::new("##", "UVrcCUjoQ", ""), &mut map);
let double = Header::make_id(
&config,
&Lexeme::new("##", "UVrcCUjoQ", ""),
&mut map,
);
assert_eq!(double, "UVrcCUjoQ-1");
let double2 =
Header::make_id(&config, &Lexeme::new("##", "UVrcCUjoQ", ""), &mut map);
let double2 = Header::make_id(
&config,
&Lexeme::new("##", "UVrcCUjoQ", ""),
&mut map,
);
assert_eq!(double2, "UVrcCUjoQ-2");
}

View file

@ -1,41 +1,45 @@
use crate::syntax::content::{Parseable, Lexeme};
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct Item {
open: bool,
pub text: String,
pub depth: Option<u8>,
}
impl Parseable for Item {
fn probe(lexeme: &Lexeme) -> bool {
(lexeme.match_as_char('-') || lexeme.match_as_char('+'))
&& lexeme.match_next_as_char(' ')
fn probe(_: &Lexeme) -> bool {
false
}
fn lex(_lexeme: &Lexeme) -> Item {
Item { open: false }
fn lex(_: &Lexeme) -> Item {
panic!("Attempt to lex an item directly from a lexeme")
}
fn render(&self) -> String {
if self.open {
String::from("<li>")
} else {
String::from("</li>")
}
panic!("Items should only be rendered by a list's render method")
}
}
impl Item {
pub fn new(open: bool) -> Item {
Item { open }
}
pub fn probe_end(lexeme: &Lexeme) -> bool {
lexeme.match_as_char('\n')
pub fn new(text: &str, depth: Option<u8>) -> Item {
Item {
text: String::from(text),
depth,
}
}
}
impl std::fmt::Display for Item {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Item [{}]", if self.open { "open" } else { "closed" })
write!(
f,
"Item [{}] {}",
if let Some(depth) = self.depth {
format!("D{depth}")
} else {
"<unknown>".to_string()
},
self.text,
)
}
}

View file

@ -1,15 +1,19 @@
use crate::syntax::content::{Parseable, Lexeme};
use std::fmt::Write as _;
#[derive(Debug, Clone, Eq, PartialEq)]
use crate::{
prelude::*,
syntax::content::{Lexeme, Parseable, parser::token::item::Item},
};
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct List {
open: bool,
ordered: bool,
pub ordered: bool,
pub items: Vec<Item>,
}
impl Parseable for List {
fn probe(lexeme: &Lexeme) -> bool {
(lexeme.match_as_char('-') || lexeme.match_as_char('+'))
&& lexeme.match_next_as_char(' ')
lexeme.match_either_char('-', '+') && lexeme.match_next_char(' ')
}
fn lex(_lexeme: &Lexeme) -> List {
@ -17,20 +21,57 @@ impl Parseable for List {
}
fn render(&self) -> String {
let bar = if self.open { "" } else { "/" };
let tag = if self.ordered { "ol" } else { "ul" };
let mut output = String::new();
format!("<{bar}{tag}>")
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 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;
output.push_str("<li>");
output.push_str(&item.text);
if next_level > current_level {
// Open nested list(s), keep <li> open
for _ in 0..(next_level - current_level) {
output.push_str(&format!("<{tag}>\n"));
}
} else {
// close current <li>
output.push_str("</li>");
// close nested lists inline
for _ in 0..(current_level - next_level) {
output.push_str(&format!("</{tag}></li>"));
}
output.push('\n');
}
}
format!("\n<{tag}>\n{output}</{tag}>\n\n")
}
}
impl List {
pub fn new(open: bool, ordered: bool) -> List {
List { open, ordered }
}
pub fn probe_end(lexeme: &Lexeme) -> bool {
lexeme.match_as_char('\n')
pub fn new(ordered: bool) -> List {
List {
ordered,
items: vec![],
}
}
}
@ -38,9 +79,87 @@ impl std::fmt::Display for List {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"List [{} {}]",
if self.open { "open" } else { "closed" },
"List [{} {} items]",
self.items.len(),
if self.ordered { "ordered" } else { "unordered" },
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_flat_list() {
let mut list = List::new(false);
list.items = vec![
Item::new("a", Some(0)),
Item::new("b", Some(0)),
Item::new("c", Some(0)),
];
assert_eq!(
list.render(),
"\n<ul>\n\
<li>a</li>\n\
<li>b</li>\n\
<li>c</li>\n\
</ul>\n\n"
);
}
#[test]
fn render_nested_list() {
let mut list = List::new(false);
list.items = vec![
Item::new("0Aa", Some(0)),
Item::new("4Ba", Some(4)),
Item::new("0Ca", Some(0)),
Item::new("4Da", Some(4)),
Item::new("4Db", Some(4)),
Item::new("0Ea", Some(0)),
Item::new("0Eb", Some(0)),
];
assert_eq!(
list.render(),
"\n<ul>\n\
<li>0Aa<ul>\n\
<li>4Ba</li></ul></li>\n\
<li>0Ca<ul>\n\
<li>4Da</li>\n\
<li>4Db</li></ul></li>\n\
<li>0Ea</li>\n\
<li>0Eb</li>\n\
</ul>\n\n"
);
}
#[test]
fn render_multilevel_depth_drop() {
let mut list = List::new(false);
list.items = vec![
Item::new("0Aa", Some(0)),
Item::new("4Ba", Some(4)),
Item::new("8Ca", Some(8)),
Item::new("12Da", Some(12)),
Item::new("16Ea", Some(16)),
Item::new("8Fa", Some(8)),
Item::new("0Ga", Some(0)),
];
assert_eq!(
list.render(),
"\n<ul>\n\
<li>0Aa<ul>\n\
<li>4Ba<ul>\n\
<li>8Ca<ul>\n\
<li>12Da<ul>\n\
<li>16Ea</li></ul></li></ul></li>\n\
<li>8Fa</li></ul></li></ul></li>\n\
<li>0Ga</li>\n\
</ul>\n\n"
);
}
}