summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-07-18 14:34:00 +0200
committerLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-07-18 14:34:00 +0200
commit47b7a36f36b99e346909390621b04f6691ff46d4 (patch)
treeab3d5c7ca7c81eb14fd5e2b0332e63fe68a6a7f8
parentaa62fa2ce992949d20ddbe8683ed2be0d922a568 (diff)
downloadiced-47b7a36f36b99e346909390621b04f6691ff46d4.tar.gz
iced-47b7a36f36b99e346909390621b04f6691ff46d4.tar.bz2
iced-47b7a36f36b99e346909390621b04f6691ff46d4.zip
Create `markdown` widget helpers in `iced_widget`
-rw-r--r--Cargo.toml5
-rw-r--r--examples/markdown/Cargo.toml4
-rw-r--r--examples/markdown/src/main.rs224
-rw-r--r--widget/Cargo.toml10
-rw-r--r--widget/src/helpers.rs5
-rw-r--r--widget/src/lib.rs3
-rw-r--r--widget/src/markdown.rs246
7 files changed, 277 insertions, 220 deletions
diff --git a/Cargo.toml b/Cargo.toml
index bc566bf6..d301b36d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,6 +34,8 @@ svg = ["iced_widget/svg"]
canvas = ["iced_widget/canvas"]
# Enables the `QRCode` widget
qr_code = ["iced_widget/qr_code"]
+# Enables the `markdown` widget
+markdown = ["iced_widget/markdown"]
# Enables lazy widgets
lazy = ["iced_widget/lazy"]
# Enables a debug view in native platforms (press F12)
@@ -51,7 +53,7 @@ web-colors = ["iced_renderer/web-colors"]
# Enables the WebGL backend, replacing WebGPU
webgl = ["iced_renderer/webgl"]
# Enables the syntax `highlighter` module
-highlighter = ["iced_highlighter"]
+highlighter = ["iced_highlighter", "iced_widget/highlighter"]
# Enables experimental multi-window support.
multi-window = ["iced_winit/multi-window"]
# Enables the advanced module
@@ -155,6 +157,7 @@ num-traits = "0.2"
once_cell = "1.0"
ouroboros = "0.18"
palette = "0.7"
+pulldown-cmark = "0.11"
qrcode = { version = "0.13", default-features = false }
raw-window-handle = "0.6"
resvg = "0.42"
diff --git a/examples/markdown/Cargo.toml b/examples/markdown/Cargo.toml
index 6875ee94..9404d5d2 100644
--- a/examples/markdown/Cargo.toml
+++ b/examples/markdown/Cargo.toml
@@ -7,6 +7,4 @@ publish = false
[dependencies]
iced.workspace = true
-iced.features = ["highlighter", "debug"]
-
-pulldown-cmark = "0.11"
+iced.features = ["markdown", "highlighter", "debug"]
diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs
index 1e3769ff..28b5941f 100644
--- a/examples/markdown/src/main.rs
+++ b/examples/markdown/src/main.rs
@@ -1,7 +1,4 @@
-use iced::widget::{
- self, column, container, rich_text, row, scrollable, span, text,
- text_editor,
-};
+use iced::widget::{self, markdown, row, scrollable, text_editor};
use iced::{Element, Fill, Font, Task, Theme};
pub fn main() -> iced::Result {
@@ -12,7 +9,7 @@ pub fn main() -> iced::Result {
struct Markdown {
content: text_editor::Content,
- items: Vec<Item>,
+ items: Vec<markdown::Item>,
theme: Theme,
}
@@ -30,7 +27,8 @@ impl Markdown {
(
Self {
content: text_editor::Content::with_text(INITIAL_CONTENT),
- items: parse(INITIAL_CONTENT, &theme).collect(),
+ items: markdown::parse(INITIAL_CONTENT, theme.palette())
+ .collect(),
theme,
},
widget::focus_next(),
@@ -44,8 +42,11 @@ impl Markdown {
self.content.perform(action);
if is_edit {
- self.items =
- parse(&self.content.text(), &self.theme).collect();
+ self.items = markdown::parse(
+ &self.content.text(),
+ self.theme.palette(),
+ )
+ .collect();
}
}
}
@@ -70,210 +71,3 @@ impl Markdown {
Theme::TokyoNight
}
}
-
-fn markdown<'a>(
- items: impl IntoIterator<Item = &'a Item>,
-) -> Element<'a, Message> {
- use iced::padding;
-
- let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
- Item::Heading(heading) => container(rich_text(heading))
- .padding(padding::top(if i > 0 { 8 } else { 0 }))
- .into(),
- Item::Paragraph(paragraph) => rich_text(paragraph).into(),
- Item::List { start: None, items } => column(
- items
- .iter()
- .map(|item| row!["•", rich_text(item)].spacing(10).into()),
- )
- .spacing(10)
- .into(),
- Item::List {
- start: Some(start),
- items,
- } => column(items.iter().enumerate().map(|(i, item)| {
- row![text!("{}.", i as u64 + *start), rich_text(item)]
- .spacing(10)
- .into()
- }))
- .spacing(10)
- .into(),
- Item::CodeBlock(code) => {
- container(rich_text(code).font(Font::MONOSPACE).size(12))
- .width(Fill)
- .padding(10)
- .style(container::rounded_box)
- .into()
- }
- });
-
- column(blocks).width(Fill).spacing(16).into()
-}
-
-#[derive(Debug, Clone)]
-enum Item {
- Heading(Vec<text::Span<'static>>),
- Paragraph(Vec<text::Span<'static>>),
- CodeBlock(Vec<text::Span<'static>>),
- List {
- start: Option<u64>,
- items: Vec<Vec<text::Span<'static>>>,
- },
-}
-
-fn parse<'a>(
- markdown: &'a str,
- theme: &'a Theme,
-) -> impl Iterator<Item = Item> + 'a {
- use iced::font;
- use iced::highlighter::{self, Highlighter};
- use text::Highlighter as _;
-
- let mut spans = Vec::new();
- let mut heading = None;
- let mut strong = false;
- let mut emphasis = false;
- let mut link = false;
- let mut list = Vec::new();
- let mut list_start = None;
- let mut highlighter = None;
-
- let parser = pulldown_cmark::Parser::new(markdown);
-
- // We want to keep the `spans` capacity
- #[allow(clippy::drain_collect)]
- parser.filter_map(move |event| match event {
- pulldown_cmark::Event::Start(tag) => match tag {
- pulldown_cmark::Tag::Heading { level, .. } => {
- heading = Some(level);
- None
- }
- pulldown_cmark::Tag::Strong => {
- strong = true;
- None
- }
- pulldown_cmark::Tag::Emphasis => {
- emphasis = true;
- None
- }
- pulldown_cmark::Tag::Link { .. } => {
- link = true;
- None
- }
- pulldown_cmark::Tag::List(first_item) => {
- list_start = first_item;
- None
- }
- pulldown_cmark::Tag::CodeBlock(
- pulldown_cmark::CodeBlockKind::Fenced(language),
- ) => {
- highlighter = Some(Highlighter::new(&highlighter::Settings {
- theme: highlighter::Theme::Base16Ocean,
- token: language.to_string(),
- }));
-
- None
- }
- _ => None,
- },
- pulldown_cmark::Event::End(tag) => match tag {
- pulldown_cmark::TagEnd::Heading(_) => {
- heading = None;
- Some(Item::Heading(spans.drain(..).collect()))
- }
- pulldown_cmark::TagEnd::Emphasis => {
- emphasis = false;
- None
- }
- pulldown_cmark::TagEnd::Strong => {
- strong = false;
- None
- }
- pulldown_cmark::TagEnd::Link => {
- link = false;
- None
- }
- pulldown_cmark::TagEnd::Paragraph => {
- Some(Item::Paragraph(spans.drain(..).collect()))
- }
- pulldown_cmark::TagEnd::List(_) => Some(Item::List {
- start: list_start,
- items: list.drain(..).collect(),
- }),
- pulldown_cmark::TagEnd::Item => {
- list.push(spans.drain(..).collect());
- None
- }
- pulldown_cmark::TagEnd::CodeBlock => {
- highlighter = None;
- Some(Item::CodeBlock(spans.drain(..).collect()))
- }
- _ => None,
- },
- pulldown_cmark::Event::Text(text) => {
- if let Some(highlighter) = &mut highlighter {
- 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());
-
- spans.push(span);
- }
- } else {
- let span = span(text.into_string());
-
- let span = match heading {
- None => span,
- Some(heading) => span.size(match heading {
- pulldown_cmark::HeadingLevel::H1 => 32,
- pulldown_cmark::HeadingLevel::H2 => 28,
- pulldown_cmark::HeadingLevel::H3 => 24,
- pulldown_cmark::HeadingLevel::H4 => 20,
- pulldown_cmark::HeadingLevel::H5 => 16,
- pulldown_cmark::HeadingLevel::H6 => 16,
- }),
- };
-
- 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 =
- span.color_maybe(link.then(|| theme.palette().primary));
-
- spans.push(span);
- }
-
- None
- }
- pulldown_cmark::Event::Code(code) => {
- spans.push(span(code.into_string()).font(Font::MONOSPACE));
- None
- }
- pulldown_cmark::Event::SoftBreak => {
- spans.push(span(" "));
- None
- }
- pulldown_cmark::Event::HardBreak => {
- spans.push(span("\n"));
- None
- }
- _ => None,
- })
-}
diff --git a/widget/Cargo.toml b/widget/Cargo.toml
index 498a768b..2f483b79 100644
--- a/widget/Cargo.toml
+++ b/widget/Cargo.toml
@@ -22,8 +22,10 @@ lazy = ["ouroboros"]
image = ["iced_renderer/image"]
svg = ["iced_renderer/svg"]
canvas = ["iced_renderer/geometry"]
-qr_code = ["canvas", "qrcode"]
+qr_code = ["canvas", "dep:qrcode"]
wgpu = ["iced_renderer/wgpu"]
+markdown = ["dep:pulldown-cmark"]
+highlighter = ["dep:iced_highlighter"]
advanced = []
[dependencies]
@@ -41,3 +43,9 @@ ouroboros.optional = true
qrcode.workspace = true
qrcode.optional = true
+
+pulldown-cmark.workspace = true
+pulldown-cmark.optional = true
+
+iced_highlighter.workspace = true
+iced_highlighter.optional = true
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 0390079f..43fee845 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -7,6 +7,7 @@ use crate::core;
use crate::core::widget::operation;
use crate::core::{Element, Length, Pixels, Widget};
use crate::keyed;
+use crate::markdown::{self};
use crate::overlay;
use crate::pick_list::{self, PickList};
use crate::progress_bar::{self, ProgressBar};
@@ -702,6 +703,10 @@ pub fn span<'a, Font>(
text::Span::new(text)
}
+#[cfg(feature = "markdown")]
+#[doc(inline)]
+pub use markdown::view as markdown;
+
/// Creates a new [`Checkbox`].
///
/// [`Checkbox`]: crate::Checkbox
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 00e9aaa4..115a29e5 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -130,5 +130,8 @@ pub mod qr_code;
#[doc(no_inline)]
pub use qr_code::QRCode;
+#[cfg(feature = "markdown")]
+pub mod markdown;
+
pub use crate::core::theme::{self, Theme};
pub use renderer::Renderer;
diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs
new file mode 100644
index 00000000..bbb5b463
--- /dev/null
+++ b/widget/src/markdown.rs
@@ -0,0 +1,246 @@
+//! Parse and display Markdown.
+//!
+//! You can enable the `highlighter` feature for syntax highligting
+//! in code blocks.
+//!
+//! Only the variants of [`Item`] are currently supported.
+use crate::core::font::{self, Font};
+use crate::core::padding;
+use crate::core::theme::{self, Theme};
+use crate::core::{self, Element, Length};
+use crate::{column, container, rich_text, row, span, text};
+
+/// A Markdown item.
+#[derive(Debug, Clone)]
+pub enum Item {
+ /// A heading.
+ Heading(Vec<text::Span<'static>>),
+ /// A paragraph.
+ Paragraph(Vec<text::Span<'static>>),
+ /// A code block.
+ ///
+ /// You can enable the `highlighter` feature for syntax highligting.
+ CodeBlock(Vec<text::Span<'static>>),
+ /// A list.
+ List {
+ /// The first number of the list, if it is ordered.
+ start: Option<u64>,
+ /// The items of the list.
+ items: Vec<Vec<text::Span<'static>>>,
+ },
+}
+
+/// Parse the given Markdown content.
+pub fn parse(
+ markdown: &str,
+ palette: theme::Palette,
+) -> impl Iterator<Item = Item> + '_ {
+ let mut spans = Vec::new();
+ let mut heading = None;
+ let mut strong = false;
+ let mut emphasis = false;
+ let mut link = false;
+ let mut list = Vec::new();
+ let mut list_start = None;
+
+ #[cfg(feature = "highlighter")]
+ let mut highlighter = None;
+
+ let parser = pulldown_cmark::Parser::new(markdown);
+
+ // We want to keep the `spans` capacity
+ #[allow(clippy::drain_collect)]
+ parser.filter_map(move |event| match event {
+ pulldown_cmark::Event::Start(tag) => match tag {
+ pulldown_cmark::Tag::Heading { level, .. } => {
+ heading = Some(level);
+ None
+ }
+ pulldown_cmark::Tag::Strong => {
+ strong = true;
+ None
+ }
+ pulldown_cmark::Tag::Emphasis => {
+ emphasis = true;
+ None
+ }
+ pulldown_cmark::Tag::Link { .. } => {
+ link = true;
+ None
+ }
+ pulldown_cmark::Tag::List(first_item) => {
+ list_start = first_item;
+ None
+ }
+ pulldown_cmark::Tag::CodeBlock(
+ pulldown_cmark::CodeBlockKind::Fenced(_language),
+ ) => {
+ #[cfg(feature = "highlighter")]
+ {
+ use iced_highlighter::{self, Highlighter};
+ use text::Highlighter as _;
+
+ highlighter =
+ Some(Highlighter::new(&iced_highlighter::Settings {
+ theme: iced_highlighter::Theme::Base16Ocean,
+ token: _language.to_string(),
+ }));
+ }
+
+ None
+ }
+ _ => None,
+ },
+ pulldown_cmark::Event::End(tag) => match tag {
+ pulldown_cmark::TagEnd::Heading(_) => {
+ heading = None;
+ Some(Item::Heading(spans.drain(..).collect()))
+ }
+ pulldown_cmark::TagEnd::Emphasis => {
+ emphasis = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Strong => {
+ strong = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Link => {
+ link = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Paragraph => {
+ Some(Item::Paragraph(spans.drain(..).collect()))
+ }
+ pulldown_cmark::TagEnd::List(_) => Some(Item::List {
+ start: list_start,
+ items: list.drain(..).collect(),
+ }),
+ pulldown_cmark::TagEnd::Item => {
+ list.push(spans.drain(..).collect());
+ None
+ }
+ pulldown_cmark::TagEnd::CodeBlock => {
+ #[cfg(feature = "highlighter")]
+ {
+ highlighter = None;
+ }
+
+ Some(Item::CodeBlock(spans.drain(..).collect()))
+ }
+ _ => None,
+ },
+ pulldown_cmark::Event::Text(text) => {
+ #[cfg(feature = "highlighter")]
+ if let Some(highlighter) = &mut highlighter {
+ use text::Highlighter as _;
+
+ 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());
+
+ spans.push(span);
+ }
+
+ return None;
+ }
+
+ let span = span(text.into_string());
+
+ let span = match heading {
+ None => span,
+ Some(heading) => span.size(match heading {
+ pulldown_cmark::HeadingLevel::H1 => 32,
+ pulldown_cmark::HeadingLevel::H2 => 28,
+ pulldown_cmark::HeadingLevel::H3 => 24,
+ pulldown_cmark::HeadingLevel::H4 => 20,
+ pulldown_cmark::HeadingLevel::H5 => 16,
+ pulldown_cmark::HeadingLevel::H6 => 16,
+ }),
+ };
+
+ 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 = span.color_maybe(link.then_some(palette.primary));
+
+ spans.push(span);
+
+ None
+ }
+ pulldown_cmark::Event::Code(code) => {
+ spans.push(span(code.into_string()).font(Font::MONOSPACE));
+ None
+ }
+ pulldown_cmark::Event::SoftBreak => {
+ spans.push(span(" "));
+ None
+ }
+ pulldown_cmark::Event::HardBreak => {
+ spans.push(span("\n"));
+ None
+ }
+ _ => None,
+ })
+}
+
+/// Display a bunch of Markdown items.
+///
+/// You can obtain the items with [`parse`].
+pub fn view<'a, Message, Renderer>(
+ items: impl IntoIterator<Item = &'a Item>,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
+ Item::Heading(heading) => container(rich_text(heading))
+ .padding(padding::top(if i > 0 { 8 } else { 0 }))
+ .into(),
+ Item::Paragraph(paragraph) => rich_text(paragraph).into(),
+ Item::List { start: None, items } => column(
+ items
+ .iter()
+ .map(|item| row!["•", rich_text(item)].spacing(10).into()),
+ )
+ .spacing(10)
+ .into(),
+ Item::List {
+ start: Some(start),
+ items,
+ } => column(items.iter().enumerate().map(|(i, item)| {
+ row![text!("{}.", i as u64 + *start), rich_text(item)]
+ .spacing(10)
+ .into()
+ }))
+ .spacing(10)
+ .into(),
+ Item::CodeBlock(code) => {
+ container(rich_text(code).font(Font::MONOSPACE).size(12))
+ .width(Length::Fill)
+ .padding(10)
+ .style(container::rounded_box)
+ .into()
+ }
+ });
+
+ Element::new(column(blocks).width(Length::Fill).spacing(16))
+}