feat: add support for *non-interactive* checkmarks in markdown

This commit is contained in:
pml68 2025-07-24 21:35:06 +02:00 • committed by Héctor Ramón Jiménez
parent 8b2b302762
commit 798e777772
No known key found for this signature in database
GPG key ID: 7CC46565708259A7

View file

@ -51,7 +51,9 @@ use crate::core::theme;
use crate::core::{ use crate::core::{
self, Color, Element, Length, Padding, Pixels, Theme, color, self, Color, Element, Length, Padding, Pixels, Theme, color,
}; };
use crate::{column, container, rich_text, row, rule, scrollable, span, text}; use crate::{
checkbox, column, container, rich_text, row, rule, scrollable, span, text,
};
use std::borrow::BorrowMut; use std::borrow::BorrowMut;
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
@ -208,7 +210,7 @@ pub enum Item {
/// The first number of the list, if it is ordered. /// The first number of the list, if it is ordered.
start: Option<u64>, start: Option<u64>,
/// The items of the list. /// The items of the list.
items: Vec<Vec<Item>>, items: Vec<ListItem>,
}, },
/// An image. /// An image.
Image { Image {
@ -350,6 +352,19 @@ impl Span {
} }
} }
/// The item of a list.
#[derive(Debug, Clone)]
pub struct ListItem {
checked: Option<bool>,
items: Vec<Item>,
}
impl ListItem {
fn push(&mut self, item: Item) {
self.items.push(item);
}
}
/// Parse the given Markdown content. /// Parse the given Markdown content.
/// ///
/// # Example /// # Example
@ -503,7 +518,7 @@ fn parse_with<'a>(
struct List { struct List {
start: Option<u64>, start: Option<u64>,
items: Vec<Vec<Item>>, items: Vec<ListItem>,
} }
let broken_links = Rc::new(RefCell::new(HashSet::new())); let broken_links = Rc::new(RefCell::new(HashSet::new()));
@ -529,7 +544,8 @@ fn parse_with<'a>(
pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
| pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
| pulldown_cmark::Options::ENABLE_TABLES | pulldown_cmark::Options::ENABLE_TABLES
| pulldown_cmark::Options::ENABLE_STRIKETHROUGH, | pulldown_cmark::Options::ENABLE_STRIKETHROUGH
| pulldown_cmark::Options::ENABLE_TASKLISTS,
{ {
let references = state.borrow().references.clone(); let references = state.borrow().references.clone();
let broken_links = broken_links.clone(); let broken_links = broken_links.clone();
@ -637,7 +653,10 @@ fn parse_with<'a>(
} }
pulldown_cmark::Tag::Item => { pulldown_cmark::Tag::Item => {
if let Some(Scope::List(list)) = stack.last_mut() { if let Some(Scope::List(list)) = stack.last_mut() {
list.items.push(Vec::new()); list.items.push(ListItem {
checked: None,
items: Vec::new(),
});
} }
None None
@ -970,6 +989,15 @@ fn parse_with<'a>(
pulldown_cmark::Event::Rule => { pulldown_cmark::Event::Rule => {
produce(state.borrow_mut(), &mut stack, Item::Rule, source) produce(state.borrow_mut(), &mut stack, Item::Rule, source)
} }
pulldown_cmark::Event::TaskListMarker(checked) => {
if let Some(Scope::List(list)) = stack.last_mut()
&& let Some(item) = list.items.last_mut()
{
item.checked = Some(checked);
}
None
}
_ => None, _ => None,
}) })
} }
@ -1280,18 +1308,28 @@ where
pub fn unordered_list<'a, Message, Theme, Renderer>( pub fn unordered_list<'a, Message, Theme, Renderer>(
viewer: &impl Viewer<'a, Message, Theme, Renderer>, viewer: &impl Viewer<'a, Message, Theme, Renderer>,
settings: Settings, settings: Settings,
items: &'a [Vec<Item>], items: &'a [ListItem],
) -> Element<'a, Message, Theme, Renderer> ) -> Element<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
Theme: Catalog + 'a, Theme: Catalog + 'a,
Renderer: core::text::Renderer<Font = Font> + 'a, Renderer: core::text::Renderer<Font = Font> + 'a,
{ {
column(items.iter().map(|items| { column(items.iter().map(|list_item| {
row![ row![
text("•").size(settings.text_size), list_item
.checked
.map(|is_checked| Element::from(
checkbox(is_checked).text_size(settings.text_size)
))
.unwrap_or_else(|| Element::from(
text("•")
.width(settings.text_size)
.center()
.size(settings.text_size)
)),
view_with( view_with(
items, &list_item.items,
Settings { Settings {
spacing: settings.spacing * 0.6, spacing: settings.spacing * 0.6,
..settings ..settings
@ -1313,7 +1351,7 @@ pub fn ordered_list<'a, Message, Theme, Renderer>(
viewer: &impl Viewer<'a, Message, Theme, Renderer>, viewer: &impl Viewer<'a, Message, Theme, Renderer>,
settings: Settings, settings: Settings,
start: u64, start: u64,
items: &'a [Vec<Item>], items: &'a [ListItem],
) -> Element<'a, Message, Theme, Renderer> ) -> Element<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
@ -1322,14 +1360,30 @@ where
{ {
let digits = ((start + items.len() as u64).max(1) as f32).log10().ceil(); let digits = ((start + items.len() as u64).max(1) as f32).log10().ceil();
column(items.iter().enumerate().map(|(i, items)| { column(items.iter().enumerate().map(|(i, list_item)| {
row![ row![
text!("{}.", i as u64 + start) list_item
.size(settings.text_size) .checked
.align_x(alignment::Horizontal::Right) .map(|is_checked| {
.width(settings.text_size * ((digits / 2.0).ceil() + 1.0)), Element::from(
container(
checkbox(is_checked).text_size(settings.text_size),
)
.align_right(
settings.text_size * ((digits / 2.0).ceil() + 1.0),
),
)
})
.unwrap_or_else(|| Element::from(
text!("{}.", i as u64 + start)
.size(settings.text_size)
.align_x(alignment::Horizontal::Right)
.width(
settings.text_size * ((digits / 2.0).ceil() + 1.0)
),
)),
view_with( view_with(
items, &list_item.items,
Settings { Settings {
spacing: settings.spacing * 0.6, spacing: settings.spacing * 0.6,
..settings ..settings
@ -1568,7 +1622,7 @@ where
fn unordered_list( fn unordered_list(
&self, &self,
settings: Settings, settings: Settings,
items: &'a [Vec<Item>], items: &'a [ListItem],
) -> Element<'a, Message, Theme, Renderer> { ) -> Element<'a, Message, Theme, Renderer> {
unordered_list(self, settings, items) unordered_list(self, settings, items)
} }
@ -1580,7 +1634,7 @@ where
&self, &self,
settings: Settings, settings: Settings,
start: u64, start: u64,
items: &'a [Vec<Item>], items: &'a [ListItem],
) -> Element<'a, Message, Theme, Renderer> { ) -> Element<'a, Message, Theme, Renderer> {
ordered_list(self, settings, start, items) ordered_list(self, settings, start, items)
} }
@ -1638,6 +1692,7 @@ pub trait Catalog:
+ scrollable::Catalog + scrollable::Catalog
+ text::Catalog + text::Catalog
+ crate::rule::Catalog + crate::rule::Catalog
+ checkbox::Catalog
+ crate::table::Catalog + crate::table::Catalog
{ {
/// The styling class of a Markdown code block. /// The styling class of a Markdown code block.