diff options
author | 2024-07-18 14:34:00 +0200 | |
---|---|---|
committer | 2024-07-18 14:34:00 +0200 | |
commit | 47b7a36f36b99e346909390621b04f6691ff46d4 (patch) | |
tree | ab3d5c7ca7c81eb14fd5e2b0332e63fe68a6a7f8 | |
parent | aa62fa2ce992949d20ddbe8683ed2be0d922a568 (diff) | |
download | iced-47b7a36f36b99e346909390621b04f6691ff46d4.tar.gz iced-47b7a36f36b99e346909390621b04f6691ff46d4.tar.bz2 iced-47b7a36f36b99e346909390621b04f6691ff46d4.zip |
Create `markdown` widget helpers in `iced_widget`
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | examples/markdown/Cargo.toml | 4 | ||||
-rw-r--r-- | examples/markdown/src/main.rs | 224 | ||||
-rw-r--r-- | widget/Cargo.toml | 10 | ||||
-rw-r--r-- | widget/src/helpers.rs | 5 | ||||
-rw-r--r-- | widget/src/lib.rs | 3 | ||||
-rw-r--r-- | widget/src/markdown.rs | 246 |
7 files changed, 277 insertions, 220 deletions
@@ -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)) +} |