summaryrefslogtreecommitdiffstats
path: root/widget/src/markdown.rs
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-08-22 02:24:06 +0200
committerLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-08-22 02:24:06 +0200
commit4c883f12b4761c7e0b273d9a2380552336f61d96 (patch)
treee2ef505aaa87b84bad010a633a9a96891b50df7d /widget/src/markdown.rs
parent55764b923e69afa8a92b4a6cbd34ee9ddbf8a03d (diff)
downloadiced-4c883f12b4761c7e0b273d9a2380552336f61d96.tar.gz
iced-4c883f12b4761c7e0b273d9a2380552336f61d96.tar.bz2
iced-4c883f12b4761c7e0b273d9a2380552336f61d96.zip
Make `RichText` generic over data structure
... and decouple `markdown::parse` from theming
Diffstat (limited to 'widget/src/markdown.rs')
-rw-r--r--widget/src/markdown.rs254
1 files changed, 196 insertions, 58 deletions
diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs
index 2bcbde5c..ef4da0df 100644
--- a/widget/src/markdown.rs
+++ b/widget/src/markdown.rs
@@ -8,9 +8,15 @@ use crate::core::border;
use crate::core::font::{self, Font};
use crate::core::padding;
use crate::core::theme;
-use crate::core::{self, color, Color, Element, Length, Pixels, Theme};
+use crate::core::{
+ self, color, Color, Element, Length, Padding, Pixels, Theme,
+};
use crate::{column, container, rich_text, row, scrollable, span, text};
+use std::cell::{Cell, RefCell};
+use std::rc::Rc;
+
+pub use core::text::Highlight;
pub use pulldown_cmark::HeadingLevel;
pub use url::Url;
@@ -18,13 +24,13 @@ pub use url::Url;
#[derive(Debug, Clone)]
pub enum Item {
/// A heading.
- Heading(pulldown_cmark::HeadingLevel, Vec<text::Span<'static, Url>>),
+ Heading(pulldown_cmark::HeadingLevel, Text),
/// A paragraph.
- Paragraph(Vec<text::Span<'static, Url>>),
+ Paragraph(Text),
/// A code block.
///
/// You can enable the `highlighter` feature for syntax highligting.
- CodeBlock(Vec<text::Span<'static, Url>>),
+ CodeBlock(Text),
/// A list.
List {
/// The first number of the list, if it is ordered.
@@ -34,11 +40,112 @@ pub enum Item {
},
}
+/// A bunch of parsed Markdown text.
+#[derive(Debug, Clone)]
+pub struct Text {
+ spans: Vec<Span>,
+ last_style: Cell<Option<Style>>,
+ last_styled_spans: RefCell<Rc<[text::Span<'static, Url>]>>,
+}
+
+impl Text {
+ fn new(spans: Vec<Span>) -> Self {
+ Self {
+ spans,
+ last_style: Cell::default(),
+ last_styled_spans: RefCell::default(),
+ }
+ }
+
+ /// Returns the [`rich_text`] spans ready to be used for the given style.
+ ///
+ /// This method performs caching for you. It will only reallocate if the [`Style`]
+ /// provided changes.
+ pub fn spans(&self, style: Style) -> Rc<[text::Span<'static, Url>]> {
+ if Some(style) != self.last_style.get() {
+ *self.last_styled_spans.borrow_mut() =
+ self.spans.iter().map(|span| span.view(&style)).collect();
+
+ self.last_style.set(Some(style));
+ }
+
+ self.last_styled_spans.borrow().clone()
+ }
+}
+
+#[derive(Debug, Clone)]
+enum Span {
+ Standard {
+ text: String,
+ strikethrough: bool,
+ link: Option<Url>,
+ strong: bool,
+ emphasis: bool,
+ code: bool,
+ },
+ #[cfg(feature = "highlighter")]
+ Highlight {
+ text: String,
+ color: Option<Color>,
+ font: Option<Font>,
+ },
+}
+
+impl Span {
+ fn view(&self, style: &Style) -> text::Span<'static, Url> {
+ match self {
+ Span::Standard {
+ text,
+ strikethrough,
+ link,
+ strong,
+ emphasis,
+ code,
+ } => {
+ let span = span(text.clone()).strikethrough(*strikethrough);
+
+ let span = if *code {
+ span.font(Font::MONOSPACE)
+ .color(style.inline_code_color)
+ .background(style.inline_code_highlight.background)
+ .border(style.inline_code_highlight.border)
+ .padding(style.inline_code_padding)
+ } else if *strong || *emphasis {
+ span.font(Font {
+ weight: if *strong {
+ font::Weight::Bold
+ } else {
+ font::Weight::Normal
+ },
+ style: if *emphasis {
+ font::Style::Italic
+ } else {
+ font::Style::Normal
+ },
+ ..Font::default()
+ })
+ } else {
+ span
+ };
+
+ let span = if let Some(link) = link.as_ref() {
+ span.color(style.link_color).link(link.clone())
+ } else {
+ span
+ };
+
+ span
+ }
+ #[cfg(feature = "highlighter")]
+ Span::Highlight { text, color, font } => {
+ span(text.clone()).color_maybe(*color).font_maybe(*font)
+ }
+ }
+ }
+}
+
/// Parse the given Markdown content.
-pub fn parse(
- markdown: &str,
- palette: theme::Palette,
-) -> impl Iterator<Item = Item> + '_ {
+pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
struct List {
start: Option<u64>,
items: Vec<Vec<Item>>,
@@ -158,7 +265,7 @@ pub fn parse(
pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
produce(
&mut lists,
- Item::Heading(level, spans.drain(..).collect()),
+ Item::Heading(level, Text::new(spans.drain(..).collect())),
)
}
pulldown_cmark::TagEnd::Strong if !metadata && !table => {
@@ -178,7 +285,10 @@ pub fn parse(
None
}
pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
- produce(&mut lists, Item::Paragraph(spans.drain(..).collect()))
+ produce(
+ &mut lists,
+ Item::Paragraph(Text::new(spans.drain(..).collect())),
+ )
}
pulldown_cmark::TagEnd::Item if !metadata && !table => {
if spans.is_empty() {
@@ -186,7 +296,7 @@ pub fn parse(
} else {
produce(
&mut lists,
- Item::Paragraph(spans.drain(..).collect()),
+ Item::Paragraph(Text::new(spans.drain(..).collect())),
)
}
}
@@ -207,7 +317,10 @@ pub fn parse(
highlighter = None;
}
- produce(&mut lists, Item::CodeBlock(spans.drain(..).collect()))
+ produce(
+ &mut lists,
+ Item::CodeBlock(Text::new(spans.drain(..).collect())),
+ )
}
pulldown_cmark::TagEnd::MetadataBlock(_) => {
metadata = false;
@@ -227,9 +340,11 @@ pub fn parse(
for (range, highlight) in
highlighter.highlight_line(text.as_ref())
{
- let span = span(text[range].to_owned())
- .color_maybe(highlight.color())
- .font_maybe(highlight.font());
+ let span = Span::Highlight {
+ text: text[range].to_owned(),
+ color: highlight.color(),
+ font: highlight.font(),
+ };
spans.push(span);
}
@@ -237,30 +352,13 @@ pub fn parse(
return None;
}
- let span = span(text.into_string()).strikethrough(strikethrough);
-
- let span = if strong || emphasis {
- span.font(Font {
- weight: if strong {
- font::Weight::Bold
- } else {
- font::Weight::Normal
- },
- style: if emphasis {
- font::Style::Italic
- } else {
- font::Style::Normal
- },
- ..Font::default()
- })
- } else {
- span
- };
-
- let span = if let Some(link) = link.as_ref() {
- span.color(palette.primary).link(link.clone())
- } else {
- span
+ let span = Span::Standard {
+ text: text.into_string(),
+ strong,
+ emphasis,
+ strikethrough,
+ link: link.clone(),
+ code: false,
};
spans.push(span);
@@ -268,29 +366,38 @@ pub fn parse(
None
}
pulldown_cmark::Event::Code(code) if !metadata && !table => {
- let span = span(code.into_string())
- .font(Font::MONOSPACE)
- .color(Color::WHITE)
- .background(color!(0x111111))
- .border(border::rounded(2))
- .padding(padding::left(2).right(2))
- .strikethrough(strikethrough);
-
- let span = if let Some(link) = link.as_ref() {
- span.color(palette.primary).link(link.clone())
- } else {
- span
+ let span = Span::Standard {
+ text: code.into_string(),
+ strong,
+ emphasis,
+ strikethrough,
+ link: link.clone(),
+ code: true,
};
spans.push(span);
None
}
pulldown_cmark::Event::SoftBreak if !metadata && !table => {
- spans.push(span(" ").strikethrough(strikethrough));
+ spans.push(Span::Standard {
+ text: String::from(" "),
+ strikethrough,
+ strong,
+ emphasis,
+ link: link.clone(),
+ code: false,
+ });
None
}
pulldown_cmark::Event::HardBreak if !metadata && !table => {
- spans.push(span("\n"));
+ spans.push(Span::Standard {
+ text: String::from("\n"),
+ strikethrough,
+ strong,
+ emphasis,
+ link: link.clone(),
+ code: false,
+ });
None
}
_ => None,
@@ -346,12 +453,41 @@ impl Default for Settings {
}
}
+/// The text styling of some Markdown rendering in [`view`].
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Style {
+ /// The [`Highlight`] to be applied to the background of inline code.
+ pub inline_code_highlight: Highlight,
+ /// The [`Padding`] to be applied to the background of inline code.
+ pub inline_code_padding: Padding,
+ /// The [`Color`] to be applied to inline code.
+ pub inline_code_color: Color,
+ /// The [`Color`] to be applied to links.
+ pub link_color: Color,
+}
+
+impl Style {
+ /// Creates a new [`Style`] from the given [`theme::Palette`].
+ pub fn from_palette(palette: theme::Palette) -> Self {
+ Self {
+ inline_code_padding: padding::left(1).right(1),
+ inline_code_highlight: Highlight {
+ background: color!(0x111).into(),
+ border: border::rounded(2),
+ },
+ inline_code_color: Color::WHITE,
+ link_color: palette.primary,
+ }
+ }
+}
+
/// Display a bunch of Markdown items.
///
/// You can obtain the items with [`parse`].
pub fn view<'a, Theme, Renderer>(
items: impl IntoIterator<Item = &'a Item>,
settings: Settings,
+ style: Style,
) -> Element<'a, Url, Theme, Renderer>
where
Theme: Catalog + 'a,
@@ -372,7 +508,7 @@ where
let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
Item::Heading(level, heading) => {
- container(rich_text(heading).size(match level {
+ container(rich_text(heading.spans(style)).size(match level {
pulldown_cmark::HeadingLevel::H1 => h1_size,
pulldown_cmark::HeadingLevel::H2 => h2_size,
pulldown_cmark::HeadingLevel::H3 => h3_size,
@@ -388,11 +524,11 @@ where
.into()
}
Item::Paragraph(paragraph) => {
- rich_text(paragraph).size(text_size).into()
+ rich_text(paragraph.spans(style)).size(text_size).into()
}
Item::List { start: None, items } => {
column(items.iter().map(|items| {
- row![text("•").size(text_size), view(items, settings)]
+ row![text("•").size(text_size), view(items, settings, style)]
.spacing(spacing)
.into()
}))
@@ -405,7 +541,7 @@ where
} => column(items.iter().enumerate().map(|(i, items)| {
row![
text!("{}.", i as u64 + *start).size(text_size),
- view(items, settings)
+ view(items, settings, style)
]
.spacing(spacing)
.into()
@@ -415,7 +551,9 @@ where
Item::CodeBlock(code) => container(
scrollable(
container(
- rich_text(code).font(Font::MONOSPACE).size(code_size),
+ rich_text(code.spans(style))
+ .font(Font::MONOSPACE)
+ .size(code_size),
)
.padding(spacing.0 / 2.0),
)