implement listing tag

This commit is contained in:
yggverse 2026-03-09 22:05:34 +02:00
parent 666aa5caf8
commit 722a6c8bb8
2 changed files with 156 additions and 2 deletions

View file

@ -1,20 +1,20 @@
mod bold;
mod code;
mod header;
mod list;
mod pre;
mod quote;
mod reference;
mod strike;
mod underline;
use std::collections::HashMap;
use bold::Bold;
use code::Code;
use gtk::{TextBuffer, TextTag, gdk::RGBA, glib::Uri};
use header::Header;
use pre::Pre;
use quote::Quote;
use std::collections::HashMap;
use strike::Strike;
use underline::Underline;
@ -60,6 +60,8 @@ impl Tags {
// Keep in order!
let title = self.header.render(buffer);
list::render(buffer);
self.quote.render(buffer);
self.bold.render(buffer);

View file

@ -0,0 +1,152 @@
use gtk::{
TextBuffer, TextTag,
prelude::{TextBufferExt, TextBufferExtManual},
};
use regex::Regex;
const REGEX_LIST: &str =
r"(?m)^(?P<level>[ \t]*)\*[ \t]+(?:(?P<state>\[[ xX]\])[ \t]+)?(?P<text>.*)";
struct State {
pub is_checked: bool,
//tag: TextTag,
}
impl State {
fn parse(value: Option<&str>) -> Option<Self> {
if let Some(state) = value
&& (state.starts_with("[ ]") || state.starts_with("[x]"))
{
return Some(Self {
is_checked: state.starts_with("[x]"),
});
}
None
}
}
struct Item {
pub level: usize,
pub state: Option<State>,
pub text: String,
}
impl Item {
fn parse(level: &str, state: Option<&str>, text: String) -> Self {
Self {
level: level.chars().count(),
state: State::parse(state),
text,
}
}
}
/// Apply * list item `Tag` to given `TextBuffer`
pub fn render(buffer: &TextBuffer) {
let state_tag = TextTag::builder().family("monospace").build();
assert!(buffer.tag_table().add(&state_tag));
let (start, end) = buffer.bounds();
let full_content = buffer.text(&start, &end, true).to_string();
let matches: Vec<_> = Regex::new(REGEX_LIST)
.unwrap()
.captures_iter(&full_content)
.collect();
for cap in matches.into_iter().rev() {
let full_match = cap.get(0).unwrap();
let start_char_offset = full_content[..full_match.start()].chars().count() as i32;
let end_char_offset = full_content[..full_match.end()].chars().count() as i32;
let mut start_iter = buffer.iter_at_offset(start_char_offset);
let mut end_iter = buffer.iter_at_offset(end_char_offset);
buffer.delete(&mut start_iter, &mut end_iter);
let item = Item::parse(
&cap["level"],
cap.name("state").map(|m| m.as_str()),
cap["text"].into(),
);
buffer.insert_with_tags(
&mut start_iter,
&format!("{}", " ".repeat(item.level)),
&[],
);
if let Some(state) = item.state {
buffer.insert_with_tags(
&mut start_iter,
if state.is_checked { "[x] " } else { "[ ] " },
&[&state_tag],
);
}
buffer.insert_with_tags(&mut start_iter, &item.text, &[]);
}
}
#[test]
fn test_regex() {
fn item(cap: &Vec<regex::Captures<'_>>, n: usize) -> Item {
let c = cap.get(n).unwrap();
Item::parse(
&c["level"],
c.name("state").map(|m| m.as_str()),
c["text"].into(),
)
}
let cap: Vec<_> = Regex::new(REGEX_LIST)
.unwrap()
.captures_iter("Some\n* list item 1\n * list item 1.1\n * list item 1.2\n* list item 2\nand\n* list item 3\n * [x] list item 3.1\n * [ ] list item 3.2\n* list item 4\n")
.collect();
{
let item = item(&cap, 0);
assert_eq!(item.level, 0);
assert!(item.state.is_none());
assert_eq!(item.text, "list item 1");
}
{
let item = item(&cap, 1);
assert_eq!(item.level, 2);
assert!(item.state.is_none());
assert_eq!(item.text, "list item 1.1");
}
{
let item = item(&cap, 2);
assert_eq!(item.level, 2);
assert!(item.state.is_none());
assert_eq!(item.text, "list item 1.2");
}
{
let item = item(&cap, 3);
assert_eq!(item.level, 0);
assert!(item.state.is_none());
assert_eq!(item.text, "list item 2");
}
{
let item = item(&cap, 4);
assert_eq!(item.level, 0);
assert!(item.state.is_none());
assert_eq!(item.text, "list item 3");
}
{
let item = item(&cap, 5);
assert_eq!(item.level, 2);
assert!(item.state.is_some_and(|this| this.is_checked));
assert_eq!(item.text, "list item 3.1");
}
{
let item = item(&cap, 6);
assert_eq!(item.level, 2);
assert!(item.state.is_some_and(|this| !this.is_checked));
assert_eq!(item.text, "list item 3.2");
}
{
let item = item(&cap, 7);
assert_eq!(item.level, 0);
assert!(item.state.is_none());
assert_eq!(item.text, "list item 4");
}
}