From 918e45824a7abac4b0db03fe2c747b62dfd4a830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 21 Nov 2025 04:40:41 +0100 Subject: [PATCH] Stop parsing `Url` in `markdown` CommonMark accepts any URI. Co-authored-by: nico2sh --- Cargo.lock | 4 +-- examples/changelog/src/main.rs | 8 ++--- examples/markdown/Cargo.toml | 7 +++-- examples/markdown/src/main.rs | 57 ++++++++++++++++++++-------------- widget/Cargo.toml | 5 +-- widget/src/markdown.rs | 50 +++++++++++++++-------------- 6 files changed, 71 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 730d8f8b..417e30b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2598,7 +2598,6 @@ dependencies = [ "rustc-hash 2.1.1", "thiserror 2.0.17", "unicode-segmentation", - "url", ] [[package]] @@ -3203,9 +3202,10 @@ version = "0.1.0" dependencies = [ "iced", "image", - "open", "reqwest", "tokio", + "url", + "webbrowser", ] [[package]] diff --git a/examples/changelog/src/main.rs b/examples/changelog/src/main.rs index ba254c76..bbd8ce70 100644 --- a/examples/changelog/src/main.rs +++ b/examples/changelog/src/main.rs @@ -44,7 +44,7 @@ enum Message { Result<(Changelog, Vec), changelog::Error>, ), PullRequestFetched(Result), - UrlClicked(markdown::Url), + LinkClicked(markdown::Uri), TitleChanged(String), CategorySelected(changelog::Category), Next, @@ -113,7 +113,7 @@ impl Generator { Task::none() } - Message::UrlClicked(url) => { + Message::LinkClicked(url) => { let _ = webbrowser::open(url.as_str()); Task::none() @@ -281,7 +281,7 @@ impl Generator { let description = markdown(description, self.theme()) - .map(Message::UrlClicked); + .map(Message::LinkClicked); let labels = row(pull_request.labels.iter().map(|label| { @@ -351,7 +351,7 @@ impl Generator { self.theme(), ), ) - .map(Message::UrlClicked), + .map(Message::LinkClicked), ) .spacing(10), ) diff --git a/examples/markdown/Cargo.toml b/examples/markdown/Cargo.toml index 1b582e80..5022d28c 100644 --- a/examples/markdown/Cargo.toml +++ b/examples/markdown/Cargo.toml @@ -12,10 +12,13 @@ iced.features = ["markdown", "highlighter", "image", "tokio", "debug"] reqwest.version = "0.12" reqwest.features = ["json"] -image.workspace = true tokio.workspace = true +tokio.features = ["fs"] -open = "5.3" +image.workspace = true +url.workspace = true + +webbrowser = "1" # Disabled to keep amount of build dependencies low # This can be re-enabled on demand diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 6e2a79b9..87d36dc4 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -32,7 +32,7 @@ pub fn main() -> iced::Result { struct Markdown { content: markdown::Content, raw: text_editor::Content, - images: HashMap, + images: HashMap, mode: Mode, theme: Theme, now: Instant, @@ -57,9 +57,9 @@ enum Image { enum Message { Edit(text_editor::Action), Copy(String), - LinkClicked(markdown::Url), - ImageShown(markdown::Url), - ImageDownloaded(markdown::Url, Result), + LinkClicked(markdown::Uri), + ImageShown(markdown::Uri), + ImageDownloaded(markdown::Uri, Result), ToggleStream(bool), NextToken, Tick, @@ -100,20 +100,19 @@ impl Markdown { } Message::Copy(content) => clipboard::write(content), Message::LinkClicked(link) => { - let _ = open::that_in_background(link.to_string()); - + let _ = webbrowser::open(&link); Task::none() } - Message::ImageShown(url) => { - if self.images.contains_key(&url) { + Message::ImageShown(uri) => { + if self.images.contains_key(&uri) { return Task::none(); } - let _ = self.images.insert(url.clone(), Image::Loading); + let _ = self.images.insert(uri.clone(), Image::Loading); Task::perform( - download_image(url.clone()), - Message::ImageDownloaded.with(url), + download_image(uri.clone()), + Message::ImageDownloaded.with(uri), ) } Message::ImageDownloaded(url, result) => { @@ -240,19 +239,19 @@ impl Markdown { } struct CustomViewer<'a> { - images: &'a HashMap, + images: &'a HashMap, now: Instant, } impl<'a> markdown::Viewer<'a, Message> for CustomViewer<'a> { - fn on_link_click(url: markdown::Url) -> Message { + fn on_link_click(url: markdown::Uri) -> Message { Message::LinkClicked(url) } fn image( &self, _settings: markdown::Settings, - url: &'a markdown::Url, + url: &'a markdown::Uri, _title: &'a str, _alt: &markdown::Text, ) -> Element<'a, Message> { @@ -295,21 +294,31 @@ impl<'a> markdown::Viewer<'a, Message> for CustomViewer<'a> { } } -async fn download_image(url: markdown::Url) -> Result { +async fn download_image(uri: markdown::Uri) -> Result { use std::io; use tokio::task; + use url::Url; - println!("Trying to download image: {url}"); + let bytes = match Url::parse(&uri) { + Ok(url) if url.scheme() == "http" || url.scheme() == "https" => { + println!("Trying to download image: {url}"); - let client = reqwest::Client::new(); + let client = reqwest::Client::new(); - let bytes = client - .get(url) - .send() - .await? - .error_for_status()? - .bytes() - .await?; + client + .get(url) + .send() + .await? + .error_for_status()? + .bytes() + .await? + } + _ => { + return Err(Error::IOFailed(Arc::new(io::Error::other(format!( + "unsupported uri: {uri}" + ))))); + } + }; let image = task::spawn_blocking(move || { Ok::<_, Error>( diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 0769a34c..b65cfedf 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -24,7 +24,7 @@ svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "dep:qrcode"] wgpu = ["iced_renderer/wgpu-bare"] -markdown = ["dep:pulldown-cmark", "dep:url"] +markdown = ["dep:pulldown-cmark"] highlighter = ["dep:iced_highlighter"] advanced = [] crisp = [] @@ -49,6 +49,3 @@ pulldown-cmark.optional = true iced_highlighter.workspace = true iced_highlighter.optional = true - -url.workspace = true -url.optional = true diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 5e00da5e..3d6cd056 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -18,7 +18,7 @@ //! } //! //! enum Message { -//! LinkClicked(markdown::Url), +//! LinkClicked(markdown::Uri), //! } //! //! impl State { @@ -63,7 +63,11 @@ use std::sync::Arc; pub use core::text::Highlight; pub use pulldown_cmark::HeadingLevel; -pub use url::Url; + +/// A [`String`] representing a [URI] in a Markdown document +/// +/// [URI]: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier +pub type Uri = String; /// A bunch of Markdown that has been parsed. #[derive(Debug, Default)] @@ -176,7 +180,7 @@ impl Content { } /// Returns the URLs of the Markdown images present in the [`Content`]. - pub fn images(&self) -> &HashSet { + pub fn images(&self) -> &HashSet { &self.state.images } } @@ -209,7 +213,7 @@ pub enum Item { /// An image. Image { /// The destination URL of the image. - url: Url, + url: Uri, /// The title of the image. title: String, /// The alternative text of the image. @@ -249,7 +253,7 @@ pub struct Row { pub struct Text { spans: Vec, last_style: Cell>, - last_styled_spans: RefCell]>>, + last_styled_spans: RefCell]>>, } impl Text { @@ -265,7 +269,7 @@ impl Text { /// /// This method performs caching for you. It will only reallocate if the [`Style`] /// provided changes. - pub fn spans(&self, style: Style) -> Arc<[text::Span<'static, Url>]> { + pub fn spans(&self, style: Style) -> Arc<[text::Span<'static, Uri>]> { if Some(style) != self.last_style.get() { *self.last_styled_spans.borrow_mut() = self.spans.iter().map(|span| span.view(&style)).collect(); @@ -282,7 +286,7 @@ enum Span { Standard { text: String, strikethrough: bool, - link: Option, + link: Option, strong: bool, emphasis: bool, code: bool, @@ -296,7 +300,7 @@ enum Span { } impl Span { - fn view(&self, style: &Style) -> text::Span<'static, Url> { + fn view(&self, style: &Style) -> text::Span<'static, Uri> { match self { Span::Standard { text, @@ -361,7 +365,7 @@ impl Span { /// } /// /// enum Message { -/// LinkClicked(markdown::Url), +/// LinkClicked(markdown::Uri), /// } /// /// impl State { @@ -395,7 +399,7 @@ pub fn parse(markdown: &str) -> impl Iterator + '_ { struct State { leftover: String, references: HashMap, - images: HashSet, + images: HashSet, #[cfg(feature = "highlighter")] highlighter: Option, } @@ -603,15 +607,13 @@ fn parse_with<'a>( None } pulldown_cmark::Tag::Link { dest_url, .. } if !metadata => { - link = Url::parse(&dest_url).ok(); + link = Some(dest_url.into_string()); None } pulldown_cmark::Tag::Image { dest_url, title, .. } if !metadata => { - image = Url::parse(&dest_url) - .ok() - .map(|url| (url, title.into_string())); + image = Some((dest_url.into_string(), title.into_string())); None } pulldown_cmark::Tag::List(first_item) if !metadata => { @@ -1104,7 +1106,7 @@ impl From for Style { /// } /// /// enum Message { -/// LinkClicked(markdown::Url), +/// LinkClicked(markdown::Uri), /// } /// /// impl State { @@ -1132,7 +1134,7 @@ impl From for Style { pub fn view<'a, Theme, Renderer>( items: impl IntoIterator, settings: impl Into, -) -> Element<'a, Url, Theme, Renderer> +) -> Element<'a, Uri, Theme, Renderer> where Theme: Catalog + 'a, Renderer: core::text::Renderer + 'a, @@ -1209,7 +1211,7 @@ pub fn heading<'a, Message, Theme, Renderer>( level: &'a HeadingLevel, text: &'a Text, index: usize, - on_link_click: impl Fn(Url) -> Message + 'a, + on_link_click: impl Fn(Uri) -> Message + 'a, ) -> Element<'a, Message, Theme, Renderer> where Message: 'a, @@ -1251,7 +1253,7 @@ where pub fn paragraph<'a, Message, Theme, Renderer>( settings: Settings, text: &Text, - on_link_click: impl Fn(Url) -> Message + 'a, + on_link_click: impl Fn(Uri) -> Message + 'a, ) -> Element<'a, Message, Theme, Renderer> where Message: 'a, @@ -1337,7 +1339,7 @@ where pub fn code_block<'a, Message, Theme, Renderer>( settings: Settings, lines: &'a [Text], - on_link_click: impl Fn(Url) -> Message + Clone + 'a, + on_link_click: impl Fn(Uri) -> Message + Clone + 'a, ) -> Element<'a, Message, Theme, Renderer> where Message: 'a, @@ -1486,8 +1488,8 @@ where Theme: Catalog + 'a, Renderer: core::text::Renderer + 'a, { - /// Produces a message when a link is clicked with the given [`Url`]. - fn on_link_click(url: Url) -> Message; + /// Produces a message when a link is clicked with the given [`Uri`]. + fn on_link_click(url: Uri) -> Message; /// Displays an image. /// @@ -1495,7 +1497,7 @@ where fn image( &self, settings: Settings, - url: &'a Url, + url: &'a Uri, title: &'a str, alt: &Text, ) -> Element<'a, Message, Theme, Renderer> { @@ -1611,12 +1613,12 @@ where #[derive(Debug, Clone, Copy)] struct DefaultViewer; -impl<'a, Theme, Renderer> Viewer<'a, Url, Theme, Renderer> for DefaultViewer +impl<'a, Theme, Renderer> Viewer<'a, Uri, Theme, Renderer> for DefaultViewer where Theme: Catalog + 'a, Renderer: core::text::Renderer + 'a, { - fn on_link_click(url: Url) -> Url { + fn on_link_click(url: Uri) -> Uri { url } }