Add table support to markdown widget
This commit is contained in:
parent
291e615c0b
commit
dcea10f707
2 changed files with 240 additions and 52 deletions
|
|
@ -43,6 +43,7 @@
|
|||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
use crate::core::alignment;
|
||||
use crate::core::border;
|
||||
use crate::core::font::{self, Font};
|
||||
use crate::core::padding;
|
||||
|
|
@ -107,13 +108,17 @@ impl Content {
|
|||
let mut leftover = std::mem::take(&mut self.state.leftover);
|
||||
leftover.push_str(markdown);
|
||||
|
||||
let input = if leftover.trim_end().ends_with('|') {
|
||||
leftover.trim_end().trim_end_matches('|')
|
||||
} else {
|
||||
leftover.as_str()
|
||||
};
|
||||
|
||||
// Pop the last item
|
||||
let _ = self.items.pop();
|
||||
|
||||
// Re-parse last item and new text
|
||||
for (item, source, broken_links) in
|
||||
parse_with(&mut self.state, &leftover)
|
||||
{
|
||||
for (item, source, broken_links) in parse_with(&mut self.state, input) {
|
||||
if !broken_links.is_empty() {
|
||||
let _ = self.incomplete.insert(
|
||||
self.items.len(),
|
||||
|
|
@ -127,6 +132,8 @@ impl Content {
|
|||
self.items.push(item);
|
||||
}
|
||||
|
||||
self.state.leftover.push_str(&leftover[input.len()..]);
|
||||
|
||||
// Re-parse incomplete sections if new references are available
|
||||
if !self.incomplete.is_empty() {
|
||||
self.incomplete.retain(|index, section| {
|
||||
|
|
@ -215,6 +222,29 @@ pub enum Item {
|
|||
Quote(Vec<Item>),
|
||||
/// A horizontal separator.
|
||||
Rule,
|
||||
/// A table.
|
||||
Table {
|
||||
/// The columns of the table.
|
||||
columns: Vec<Column>,
|
||||
/// The rows of the table.
|
||||
rows: Vec<Row>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The column of a table.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Column {
|
||||
/// The header of the column.
|
||||
pub header: Vec<Item>,
|
||||
/// The alignment of the column.
|
||||
pub alignment: pulldown_cmark::Alignment,
|
||||
}
|
||||
|
||||
/// The row of a table.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Row {
|
||||
/// The cells of the row.
|
||||
cells: Vec<Vec<Item>>,
|
||||
}
|
||||
|
||||
/// A bunch of parsed Markdown text.
|
||||
|
|
@ -462,6 +492,12 @@ fn parse_with<'a>(
|
|||
enum Scope {
|
||||
List(List),
|
||||
Quote(Vec<Item>),
|
||||
Table {
|
||||
alignment: Vec<pulldown_cmark::Alignment>,
|
||||
columns: Vec<Column>,
|
||||
rows: Vec<Row>,
|
||||
current: Vec<Item>,
|
||||
},
|
||||
}
|
||||
|
||||
struct List {
|
||||
|
|
@ -479,7 +515,6 @@ fn parse_with<'a>(
|
|||
let mut emphasis = false;
|
||||
let mut strikethrough = false;
|
||||
let mut metadata = false;
|
||||
let mut table = false;
|
||||
let mut code_block = false;
|
||||
let mut link = None;
|
||||
let mut image = None;
|
||||
|
|
@ -535,6 +570,9 @@ fn parse_with<'a>(
|
|||
Scope::Quote(items) => {
|
||||
items.push(item);
|
||||
}
|
||||
Scope::Table { current, .. } => {
|
||||
current.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
|
|
@ -555,21 +593,19 @@ fn parse_with<'a>(
|
|||
#[allow(clippy::drain_collect)]
|
||||
parser.filter_map(move |(event, source)| match event {
|
||||
pulldown_cmark::Event::Start(tag) => match tag {
|
||||
pulldown_cmark::Tag::Strong if !metadata && !table => {
|
||||
pulldown_cmark::Tag::Strong if !metadata => {
|
||||
strong = true;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::Emphasis if !metadata && !table => {
|
||||
pulldown_cmark::Tag::Emphasis if !metadata => {
|
||||
emphasis = true;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::Strikethrough if !metadata && !table => {
|
||||
pulldown_cmark::Tag::Strikethrough if !metadata => {
|
||||
strikethrough = true;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::Link { dest_url, .. }
|
||||
if !metadata && !table =>
|
||||
{
|
||||
pulldown_cmark::Tag::Link { dest_url, .. } if !metadata => {
|
||||
match Url::parse(&dest_url) {
|
||||
Ok(url)
|
||||
if url.scheme() == "http"
|
||||
|
|
@ -584,13 +620,13 @@ fn parse_with<'a>(
|
|||
}
|
||||
pulldown_cmark::Tag::Image {
|
||||
dest_url, title, ..
|
||||
} if !metadata && !table => {
|
||||
} if !metadata => {
|
||||
image = Url::parse(&dest_url)
|
||||
.ok()
|
||||
.map(|url| (url, title.into_string()));
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
|
||||
pulldown_cmark::Tag::List(first_item) if !metadata => {
|
||||
let prev = if spans.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -616,7 +652,7 @@ fn parse_with<'a>(
|
|||
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::BlockQuote(_kind) if !metadata && !table => {
|
||||
pulldown_cmark::Tag::BlockQuote(_kind) if !metadata => {
|
||||
let prev = if spans.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -634,7 +670,7 @@ fn parse_with<'a>(
|
|||
}
|
||||
pulldown_cmark::Tag::CodeBlock(
|
||||
pulldown_cmark::CodeBlockKind::Fenced(language),
|
||||
) if !metadata && !table => {
|
||||
) if !metadata => {
|
||||
#[cfg(feature = "highlighter")]
|
||||
{
|
||||
highlighter = Some({
|
||||
|
|
@ -672,38 +708,54 @@ fn parse_with<'a>(
|
|||
metadata = true;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::Table(_) => {
|
||||
table = true;
|
||||
pulldown_cmark::Tag::Table(alignment) => {
|
||||
stack.push(Scope::Table {
|
||||
columns: Vec::with_capacity(alignment.len()),
|
||||
alignment,
|
||||
current: Vec::new(),
|
||||
rows: Vec::new(),
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::TableHead => {
|
||||
strong = true;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::TableRow => {
|
||||
let Scope::Table { rows, .. } = stack.last_mut()? else {
|
||||
return None;
|
||||
};
|
||||
|
||||
rows.push(Row { cells: Vec::new() });
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
pulldown_cmark::Event::End(tag) => match tag {
|
||||
pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut stack,
|
||||
Item::Heading(level, Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::Strong if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Heading(level) if !metadata => produce(
|
||||
state.borrow_mut(),
|
||||
&mut stack,
|
||||
Item::Heading(level, Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
),
|
||||
pulldown_cmark::TagEnd::Strong if !metadata => {
|
||||
strong = false;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Emphasis if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Emphasis if !metadata => {
|
||||
emphasis = false;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Strikethrough if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Strikethrough if !metadata => {
|
||||
strikethrough = false;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Link if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Link if !metadata => {
|
||||
link = None;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Paragraph if !metadata => {
|
||||
if spans.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -715,7 +767,7 @@ fn parse_with<'a>(
|
|||
)
|
||||
}
|
||||
}
|
||||
pulldown_cmark::TagEnd::Item if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Item if !metadata => {
|
||||
if spans.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -727,7 +779,7 @@ fn parse_with<'a>(
|
|||
)
|
||||
}
|
||||
}
|
||||
pulldown_cmark::TagEnd::List(_) if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::List(_) if !metadata => {
|
||||
let scope = stack.pop()?;
|
||||
|
||||
let Scope::List(list) = scope else {
|
||||
|
|
@ -744,9 +796,7 @@ fn parse_with<'a>(
|
|||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::BlockQuote(_kind)
|
||||
if !metadata && !table =>
|
||||
{
|
||||
pulldown_cmark::TagEnd::BlockQuote(_kind) if !metadata => {
|
||||
let scope = stack.pop()?;
|
||||
|
||||
let Scope::Quote(quote) = scope else {
|
||||
|
|
@ -760,7 +810,7 @@ fn parse_with<'a>(
|
|||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::Image if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::Image if !metadata => {
|
||||
let (url, title) = image.take()?;
|
||||
let alt = Text::new(spans.drain(..).collect());
|
||||
|
||||
|
|
@ -774,7 +824,7 @@ fn parse_with<'a>(
|
|||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
|
||||
pulldown_cmark::TagEnd::CodeBlock if !metadata => {
|
||||
code_block = false;
|
||||
|
||||
#[cfg(feature = "highlighter")]
|
||||
|
|
@ -798,12 +848,60 @@ fn parse_with<'a>(
|
|||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Table => {
|
||||
table = false;
|
||||
let scope = stack.pop()?;
|
||||
|
||||
let Scope::Table { columns, rows, .. } = scope else {
|
||||
return None;
|
||||
};
|
||||
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut stack,
|
||||
Item::Table { columns, rows },
|
||||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::TableHead => {
|
||||
strong = false;
|
||||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::TableCell => {
|
||||
if !spans.is_empty() {
|
||||
let _ = produce(
|
||||
state.borrow_mut(),
|
||||
&mut stack,
|
||||
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
);
|
||||
}
|
||||
|
||||
let Scope::Table {
|
||||
alignment,
|
||||
columns,
|
||||
rows,
|
||||
current,
|
||||
} = stack.last_mut()?
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if columns.len() < alignment.len() {
|
||||
columns.push(Column {
|
||||
header: std::mem::take(current),
|
||||
alignment: alignment[columns.len()],
|
||||
});
|
||||
} else {
|
||||
rows.last_mut()
|
||||
.expect("table row")
|
||||
.cells
|
||||
.push(std::mem::take(current));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
pulldown_cmark::Event::Text(text) if !metadata && !table => {
|
||||
pulldown_cmark::Event::Text(text) if !metadata => {
|
||||
if code_block {
|
||||
code.push_str(&text);
|
||||
|
||||
|
|
@ -844,7 +942,7 @@ fn parse_with<'a>(
|
|||
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Event::Code(code) if !metadata && !table => {
|
||||
pulldown_cmark::Event::Code(code) if !metadata => {
|
||||
let span = Span::Standard {
|
||||
text: code.into_string(),
|
||||
strong,
|
||||
|
|
@ -857,7 +955,7 @@ fn parse_with<'a>(
|
|||
spans.push(span);
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Event::SoftBreak if !metadata && !table => {
|
||||
pulldown_cmark::Event::SoftBreak if !metadata => {
|
||||
spans.push(Span::Standard {
|
||||
text: String::from(" "),
|
||||
strikethrough,
|
||||
|
|
@ -868,7 +966,7 @@ fn parse_with<'a>(
|
|||
});
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Event::HardBreak if !metadata && !table => {
|
||||
pulldown_cmark::Event::HardBreak if !metadata => {
|
||||
spans.push(Span::Standard {
|
||||
text: String::from("\n"),
|
||||
strikethrough,
|
||||
|
|
@ -1113,6 +1211,7 @@ where
|
|||
} => viewer.ordered_list(settings, *start, items),
|
||||
Item::Quote(quote) => viewer.quote(settings, quote),
|
||||
Item::Rule => viewer.rule(settings),
|
||||
Item::Table { columns, rows } => viewer.table(settings, columns, rows),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1313,6 +1412,74 @@ where
|
|||
horizontal_rule(2).into()
|
||||
}
|
||||
|
||||
/// Displays a table using the default look.
|
||||
pub fn table<'a, Message, Theme, Renderer>(
|
||||
viewer: &impl Viewer<'a, Message, Theme, Renderer>,
|
||||
settings: Settings,
|
||||
columns: &'a [Column],
|
||||
rows: &'a [Row],
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
use crate::table;
|
||||
|
||||
table(
|
||||
columns.iter().enumerate().map(move |(i, column)| {
|
||||
table::column(
|
||||
items(viewer, settings, &column.header),
|
||||
move |row: &Row| {
|
||||
if let Some(cells) = row.cells.get(i) {
|
||||
items(viewer, settings, cells)
|
||||
} else {
|
||||
text("").into()
|
||||
}
|
||||
},
|
||||
)
|
||||
.align_x(match column.alignment {
|
||||
pulldown_cmark::Alignment::None
|
||||
| pulldown_cmark::Alignment::Left => {
|
||||
alignment::Horizontal::Left
|
||||
}
|
||||
pulldown_cmark::Alignment::Center => {
|
||||
alignment::Horizontal::Center
|
||||
}
|
||||
pulldown_cmark::Alignment::Right => {
|
||||
alignment::Horizontal::Right
|
||||
}
|
||||
})
|
||||
}),
|
||||
rows,
|
||||
)
|
||||
.padding_x(settings.spacing.0)
|
||||
.padding_y(settings.spacing.0 / 2.0)
|
||||
.separator_x(0)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Displays a column of items with the default look.
|
||||
pub fn items<'a, Message, Theme, Renderer>(
|
||||
viewer: &impl Viewer<'a, Message, Theme, Renderer>,
|
||||
settings: Settings,
|
||||
items: &'a [Item],
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
column(
|
||||
items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, content)| item(viewer, settings, content, i)),
|
||||
)
|
||||
.spacing(settings.spacing.0)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A view strategy to display a Markdown [`Item`].
|
||||
pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
where
|
||||
|
|
@ -1429,6 +1596,18 @@ where
|
|||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
rule()
|
||||
}
|
||||
|
||||
/// Displays a table.
|
||||
///
|
||||
/// By default, it calls [`table`].
|
||||
fn table(
|
||||
&self,
|
||||
settings: Settings,
|
||||
columns: &'a [Column],
|
||||
rows: &'a [Row],
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
table(self, settings, columns, rows)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
|
@ -1446,7 +1625,11 @@ where
|
|||
|
||||
/// The theme catalog of Markdown items.
|
||||
pub trait Catalog:
|
||||
container::Catalog + scrollable::Catalog + rule::Catalog + text::Catalog
|
||||
container::Catalog
|
||||
+ scrollable::Catalog
|
||||
+ rule::Catalog
|
||||
+ text::Catalog
|
||||
+ crate::table::Catalog
|
||||
{
|
||||
/// The styling class of a Markdown code block.
|
||||
fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ use crate::core::{
|
|||
Widget,
|
||||
};
|
||||
|
||||
pub fn table<'a, T, Message, Theme, Renderer>(
|
||||
columns: impl IntoIterator<Item = Column<'a, T, Message, Theme, Renderer>>,
|
||||
pub fn table<'a, 'b, T, Message, Theme, Renderer>(
|
||||
columns: impl IntoIterator<Item = Column<'a, 'b, T, Message, Theme, Renderer>>,
|
||||
rows: impl IntoIterator<Item = T>,
|
||||
) -> Table<'a, Message, Theme, Renderer>
|
||||
where
|
||||
|
|
@ -22,10 +22,10 @@ where
|
|||
Table::new(columns, rows)
|
||||
}
|
||||
|
||||
pub fn column<'a, T, E, Message, Theme, Renderer>(
|
||||
pub fn column<'a, 'b, T, E, Message, Theme, Renderer>(
|
||||
header: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
view: impl Fn(T) -> E + 'a,
|
||||
) -> Column<'a, T, Message, Theme, Renderer>
|
||||
view: impl Fn(T) -> E + 'b,
|
||||
) -> Column<'a, 'b, T, Message, Theme, Renderer>
|
||||
where
|
||||
T: 'a,
|
||||
E: Into<Element<'a, Message, Theme, Renderer>>,
|
||||
|
|
@ -65,8 +65,10 @@ where
|
|||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
pub fn new<T>(
|
||||
columns: impl IntoIterator<Item = Column<'a, T, Message, Theme, Renderer>>,
|
||||
pub fn new<'b, T>(
|
||||
columns: impl IntoIterator<
|
||||
Item = Column<'a, 'b, T, Message, Theme, Renderer>,
|
||||
>,
|
||||
rows: impl IntoIterator<Item = T>,
|
||||
) -> Self
|
||||
where
|
||||
|
|
@ -122,7 +124,7 @@ where
|
|||
width,
|
||||
height,
|
||||
padding_x: 10.0,
|
||||
padding_y: 10.0,
|
||||
padding_y: 5.0,
|
||||
separator_x: 1.0,
|
||||
separator_y: 1.0,
|
||||
class: Theme::default(),
|
||||
|
|
@ -526,19 +528,22 @@ where
|
|||
|
||||
pub struct Column<
|
||||
'a,
|
||||
'b,
|
||||
T,
|
||||
Message,
|
||||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> {
|
||||
header: Element<'a, Message, Theme, Renderer>,
|
||||
view: Box<dyn Fn(T) -> Element<'a, Message, Theme, Renderer> + 'a>,
|
||||
view: Box<dyn Fn(T) -> Element<'a, Message, Theme, Renderer> + 'b>,
|
||||
width: Length,
|
||||
align_x: alignment::Horizontal,
|
||||
align_y: alignment::Vertical,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme, Renderer> Column<'a, T, Message, Theme, Renderer> {
|
||||
impl<'a, 'b, T, Message, Theme, Renderer>
|
||||
Column<'a, 'b, T, Message, Theme, Renderer>
|
||||
{
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue