From b6d92157f3ac9b2cb9f3741163329deb325f0d60 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 13 Nov 2025 14:00:53 +0100 Subject: [PATCH 01/14] Add StyledText type --- api/node/rust/interpreter/value.rs | 5 +- internal/compiler/builtin_macros.rs | 7 +- internal/compiler/expression_tree.rs | 1 + internal/compiler/generator/rust.rs | 1 + internal/compiler/langtype.rs | 6 + internal/compiler/llr/expression.rs | 1 + internal/compiler/typeregister.rs | 1 + internal/core/api.rs | 531 ++++++++++++++++++++++ internal/core/rtti.rs | 1 + internal/interpreter/api.rs | 10 + internal/interpreter/dynamic_item_tree.rs | 4 +- internal/interpreter/eval.rs | 2 + 12 files changed, 566 insertions(+), 4 deletions(-) diff --git a/api/node/rust/interpreter/value.rs b/api/node/rust/interpreter/value.rs index 4b2bb0db15e..2a675e223a3 100644 --- a/api/node/rust/interpreter/value.rs +++ b/api/node/rust/interpreter/value.rs @@ -25,6 +25,7 @@ pub enum JsValueType { Struct, Brush, Image, + StyledText, } impl From for JsValueType { @@ -37,6 +38,7 @@ impl From for JsValueType { slint_interpreter::ValueType::Struct => JsValueType::Struct, slint_interpreter::ValueType::Brush => JsValueType::Brush, slint_interpreter::ValueType::Image => JsValueType::Image, + slint_interpreter::ValueType::StyledText => JsValueType::StyledText, _ => JsValueType::Void, } } @@ -290,7 +292,8 @@ pub fn to_value(env: &Env, unknown: JsUnknown, typ: &Type) -> Result { | Type::Easing | Type::PathData | Type::LayoutCache - | Type::ElementReference => Err(napi::Error::from_reason("reason")), + | Type::ElementReference + | Type::StyledText => Err(napi::Error::from_reason("reason")), } } diff --git a/internal/compiler/builtin_macros.rs b/internal/compiler/builtin_macros.rs index 03f6fa3115e..392e39c5d72 100644 --- a/internal/compiler/builtin_macros.rs +++ b/internal/compiler/builtin_macros.rs @@ -343,7 +343,12 @@ fn to_debug_string( Type::Float32 | Type::Int32 => expr.maybe_convert_to(Type::String, node, diag), Type::String => expr, // TODO - Type::Color | Type::Brush | Type::Image | Type::Easing | Type::Array(_) => { + Type::Color + | Type::Brush + | Type::Image + | Type::Easing + | Type::StyledText + | Type::Array(_) => { Expression::StringLiteral("".into()) } Type::Duration diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index 0cb92d3fd32..66f81a5e761 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -1427,6 +1427,7 @@ impl Expression { Expression::EnumerationValue(enumeration.clone().default_value()) } Type::ComponentFactory => Expression::EmptyComponentFactory, + Type::StyledText => Expression::Invalid, } } diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index 61721577b99..c1a4dc47a4a 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -91,6 +91,7 @@ pub fn rust_primitive_type(ty: &Type) -> Option { Type::Percent => Some(quote!(f32)), Type::Bool => Some(quote!(bool)), Type::Image => Some(quote!(sp::Image)), + Type::StyledText => Some(quote!(sp::StyledText)), Type::Struct(s) => { struct_name_to_tokens(&s.name).or_else(|| { let elem = diff --git a/internal/compiler/langtype.rs b/internal/compiler/langtype.rs index 3dbaac5f95d..8dae242d7a7 100644 --- a/internal/compiler/langtype.rs +++ b/internal/compiler/langtype.rs @@ -64,6 +64,8 @@ pub enum Type { /// This is a `SharedArray` LayoutCache, + + StyledText, } impl core::cmp::PartialEq for Type { @@ -104,6 +106,7 @@ impl core::cmp::PartialEq for Type { Type::UnitProduct(a) => matches!(other, Type::UnitProduct(b) if a == b), Type::ElementReference => matches!(other, Type::ElementReference), Type::LayoutCache => matches!(other, Type::LayoutCache), + Type::StyledText => matches!(other, Type::StyledText), } } } @@ -178,6 +181,7 @@ impl Display for Type { } Type::ElementReference => write!(f, "element ref"), Type::LayoutCache => write!(f, "layout cache"), + Type::StyledText => write!(f, "styled-text"), } } } @@ -213,6 +217,7 @@ impl Type { | Self::Array(_) | Self::Brush | Self::InferredProperty + | Self::StyledText ) } @@ -314,6 +319,7 @@ impl Type { Type::UnitProduct(_) => None, Type::ElementReference => None, Type::LayoutCache => None, + Type::StyledText => None, } } diff --git a/internal/compiler/llr/expression.rs b/internal/compiler/llr/expression.rs index c131c061e5f..5c2a6f8da01 100644 --- a/internal/compiler/llr/expression.rs +++ b/internal/compiler/llr/expression.rs @@ -267,6 +267,7 @@ impl Expression { Expression::EnumerationValue(enumeration.clone().default_value()) } Type::ComponentFactory => Expression::EmptyComponentFactory, + Type::StyledText => return None, }) } diff --git a/internal/compiler/typeregister.rs b/internal/compiler/typeregister.rs index 8f4c496de9d..ba3df4d6b0f 100644 --- a/internal/compiler/typeregister.rs +++ b/internal/compiler/typeregister.rs @@ -397,6 +397,7 @@ impl TypeRegister { register.insert_type(Type::Angle); register.insert_type(Type::Brush); register.insert_type(Type::Rem); + register.insert_type(Type::StyledText); register.types.insert("Point".into(), logical_point_type().into()); BUILTIN.with(|e| e.enums.fill_register(&mut register)); diff --git a/internal/core/api.rs b/internal/core/api.rs index 88066bb90a1..180357d540e 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -1219,3 +1219,534 @@ pub fn set_xdg_app_id(app_id: impl Into) -> Result<(), PlatformErr |ctx| ctx.set_xdg_app_id(app_id.into()), ) } + +#[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] +pub enum Style { + Emphasis, + Strong, + Strikethrough, + Code, + Link, + Underline, + Color(crate::Color), +} + +#[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] +pub struct FormattedSpan { + pub range: core::ops::Range, + pub style: Style, +} + +#[cfg(feature = "experimental-rich-text")] +#[derive(Clone, Debug)] +enum ListItemType { + Ordered(u64), + Unordered, +} + +#[cfg(feature = "experimental-rich-text")] +#[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] +pub struct StyledTextParagraph { + pub text: std::string::String, + pub formatting: std::vec::Vec, + pub links: std::vec::Vec<(std::ops::Range, std::string::String)>, +} + +#[cfg(feature = "experimental-rich-text")] +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum StyledTextError<'a> { + #[error("Spans are unbalanced: stack already empty when popped")] + Pop, + #[error("Spans are unbalanced: stack contained items at end of function")] + NotEmpty, + #[error("Paragraph not started")] + ParagraphNotStarted, + #[error("Unimplemented: {:?}", .0)] + UnimplementedTag(pulldown_cmark::Tag<'a>), + #[error("Unimplemented: {:?}", .0)] + UnimplementedEvent(pulldown_cmark::Event<'a>), + #[error("Unimplemented: {}", .0)] + UnimplementedHtmlEvent(std::string::String), + #[error("Unimplemented html tag: {}", .0)] + UnimplementedHtmlTag(std::string::String), + #[error("Unexpected {} attribute in html {}", .0, .1)] + UnexpectedAttribute(std::string::String, std::string::String), + #[error("Missing color attribute in html {}", .0)] + MissingColor(std::string::String), + #[error("Closing html tag doesn't match the opening tag. Expected {}, got {}", .0, .1)] + ClosingTagMismatch(&'a str, std::string::String), +} + +/// Internal styled text type +#[derive(Debug, PartialEq, Clone, Default)] +#[allow(missing_docs)] +pub struct StyledText { + #[cfg(feature = "experimental-rich-text")] + pub paragraphs: std::vec::Vec, +} + +#[cfg(feature = "experimental-rich-text")] +#[allow(missing_docs)] +impl StyledText { + fn begin_paragraph(&mut self, indentation: u32, list_item_type: Option) { + let mut text = std::string::String::with_capacity(indentation as usize * 4); + for _ in 0..indentation { + text.push_str(" "); + } + match list_item_type { + Some(ListItemType::Unordered) => { + if indentation % 3 == 0 { + text.push_str("• ") + } else if indentation % 3 == 1 { + text.push_str("◦ ") + } else { + text.push_str("▪ ") + } + } + Some(ListItemType::Ordered(num)) => text.push_str(&std::format!("{}. ", num)), + None => {} + }; + self.paragraphs.push(StyledTextParagraph { + text, + formatting: Default::default(), + links: Default::default(), + }); + } + + pub fn parse(string: &str) -> Result> { + let parser = + pulldown_cmark::Parser::new_ext(string, pulldown_cmark::Options::ENABLE_STRIKETHROUGH); + + let mut styled_text = StyledText::default(); + let mut list_state_stack: std::vec::Vec> = std::vec::Vec::new(); + let mut style_stack = std::vec::Vec::new(); + let mut current_url = None; + + for event in parser { + let indentation = list_state_stack.len().saturating_sub(1) as _; + + match event { + pulldown_cmark::Event::SoftBreak | pulldown_cmark::Event::HardBreak => { + styled_text.begin_paragraph(indentation, None); + } + pulldown_cmark::Event::End(pulldown_cmark::TagEnd::List(_)) => { + if list_state_stack.pop().is_none() { + return Err(StyledTextError::Pop); + } + } + pulldown_cmark::Event::End( + pulldown_cmark::TagEnd::Paragraph | pulldown_cmark::TagEnd::Item, + ) => {} + pulldown_cmark::Event::Start(tag) => { + let style = match tag { + pulldown_cmark::Tag::Paragraph => { + styled_text.begin_paragraph(indentation, None); + continue; + } + pulldown_cmark::Tag::Item => { + styled_text.begin_paragraph( + indentation, + Some(match list_state_stack.last().copied() { + Some(Some(index)) => ListItemType::Ordered(index), + _ => ListItemType::Unordered, + }), + ); + if let Some(state) = list_state_stack.last_mut() { + *state = state.map(|state| state + 1); + } + continue; + } + pulldown_cmark::Tag::List(index) => { + list_state_stack.push(index); + continue; + } + pulldown_cmark::Tag::Strong => Style::Strong, + pulldown_cmark::Tag::Emphasis => Style::Emphasis, + pulldown_cmark::Tag::Strikethrough => Style::Strikethrough, + pulldown_cmark::Tag::Link { dest_url, .. } => { + current_url = Some(dest_url); + Style::Link + } + + pulldown_cmark::Tag::Heading { .. } + | pulldown_cmark::Tag::Image { .. } + | pulldown_cmark::Tag::DefinitionList + | pulldown_cmark::Tag::DefinitionListTitle + | pulldown_cmark::Tag::DefinitionListDefinition + | pulldown_cmark::Tag::TableHead + | pulldown_cmark::Tag::TableRow + | pulldown_cmark::Tag::TableCell + | pulldown_cmark::Tag::HtmlBlock + | pulldown_cmark::Tag::Superscript + | pulldown_cmark::Tag::Subscript + | pulldown_cmark::Tag::Table(_) + | pulldown_cmark::Tag::MetadataBlock(_) + | pulldown_cmark::Tag::BlockQuote(_) + | pulldown_cmark::Tag::CodeBlock(_) + | pulldown_cmark::Tag::FootnoteDefinition(_) => { + return Err(StyledTextError::UnimplementedTag(tag)); + } + }; + + style_stack.push(( + style, + styled_text + .paragraphs + .last() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .len(), + )); + } + pulldown_cmark::Event::Text(text) => { + styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .push_str(&text); + } + pulldown_cmark::Event::End(_) => { + let (style, start) = if let Some(value) = style_stack.pop() { + value + } else { + return Err(StyledTextError::Pop); + }; + + let paragraph = styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)?; + let end = paragraph.text.len(); + + if let Some(url) = current_url.take() { + paragraph.links.push((start..end, url.into())); + } + + paragraph.formatting.push(FormattedSpan { range: start..end, style }); + } + pulldown_cmark::Event::Code(text) => { + let paragraph = styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)?; + let start = paragraph.text.len(); + paragraph.text.push_str(&text); + paragraph.formatting.push(FormattedSpan { + range: start..paragraph.text.len(), + style: Style::Code, + }); + } + pulldown_cmark::Event::InlineHtml(html) => { + if html.starts_with(" "", + Style::Underline => "", + other => std::unreachable!( + "Got unexpected closing style {:?} with html {}. This error should have been caught earlier.", + other, + html + ), + }; + + if (&*html) != expected_tag { + return Err(StyledTextError::ClosingTagMismatch( + expected_tag, + (&*html).into(), + )); + } + + let paragraph = styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)?; + let end = paragraph.text.len(); + paragraph.formatting.push(FormattedSpan { range: start..end, style }); + } else { + let mut expecting_color_attribute = false; + + for token in htmlparser::Tokenizer::from(&*html) { + match token { + Ok(htmlparser::Token::ElementStart { local: tag_type, .. }) => { + match &*tag_type { + "u" => { + style_stack.push(( + Style::Underline, + styled_text + .paragraphs + .last() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .len(), + )); + } + "font" => { + expecting_color_attribute = true; + } + _ => { + return Err(StyledTextError::UnimplementedHtmlTag( + (&*tag_type).into(), + )); + } + } + } + Ok(htmlparser::Token::Attribute { + local: key, + value: Some(value), + .. + }) => match &*key { + "color" => { + if !expecting_color_attribute { + return Err(StyledTextError::UnexpectedAttribute( + (&*key).into(), + (&*html).into(), + )); + } + expecting_color_attribute = false; + + let value = + i_slint_common::color_parsing::parse_color_literal( + &*value, + ) + .or_else(|| { + i_slint_common::color_parsing::named_colors() + .get(&*value) + .copied() + }) + .expect("invalid color value"); + + style_stack.push(( + Style::Color(crate::Color::from_argb_encoded(value)), + styled_text + .paragraphs + .last() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .len(), + )); + } + _ => { + return Err(StyledTextError::UnexpectedAttribute( + (&*key).into(), + (&*html).into(), + )); + } + }, + Ok(htmlparser::Token::ElementEnd { .. }) => {} + _ => { + return Err(StyledTextError::UnimplementedHtmlEvent( + std::format!("{:?}", token), + )); + } + } + } + + if expecting_color_attribute { + return Err(StyledTextError::MissingColor((&*html).into())); + } + } + } + pulldown_cmark::Event::Rule + | pulldown_cmark::Event::TaskListMarker(_) + | pulldown_cmark::Event::FootnoteReference(_) + | pulldown_cmark::Event::InlineMath(_) + | pulldown_cmark::Event::DisplayMath(_) + | pulldown_cmark::Event::Html(_) => { + return Err(StyledTextError::UnimplementedEvent(event)); + } + } + } + + if !style_stack.is_empty() { + return Err(StyledTextError::NotEmpty); + } + + Ok(styled_text) + } +} + +#[cfg(feature = "experimental-rich-text")] +#[test] +fn markdown_parsing() { + assert_eq!( + StyledText::parse("hello *world*").unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![FormattedSpan { range: 6..11, style: Style::Emphasis }], + links: std::vec![] + }] + ); + + assert_eq!( + StyledText::parse( + " +- line 1 +- line 2 + " + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "• line 1".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: "• line 2".into(), + formatting: std::vec![], + links: std::vec![] + } + ] + ); + + assert_eq!( + StyledText::parse( + " +1. a +2. b +4. c + " + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "1. a".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: "2. b".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: "3. c".into(), + formatting: std::vec![], + links: std::vec![] + } + ] + ); + + assert_eq!( + StyledText::parse( + " +Normal _italic_ **strong** ~~strikethrough~~ `code` +new *line* +" + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "Normal italic strong strikethrough code".into(), + formatting: std::vec![ + FormattedSpan { range: 7..13, style: Style::Emphasis }, + FormattedSpan { range: 14..20, style: Style::Strong }, + FormattedSpan { range: 21..34, style: Style::Strikethrough }, + FormattedSpan { range: 35..39, style: Style::Code } + ], + links: std::vec![] + }, + StyledTextParagraph { + text: "new line".into(), + formatting: std::vec![FormattedSpan { range: 4..8, style: Style::Emphasis },], + links: std::vec![] + } + ] + ); + + assert_eq!( + StyledText::parse( + " +- root + - child + - grandchild + - great grandchild +" + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "• root".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: " ◦ child".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: " ▪ grandchild".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: " • great grandchild".into(), + formatting: std::vec![], + links: std::vec![] + }, + ] + ); + + assert_eq!( + StyledText::parse("hello [*world*](https://example.com)").unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![ + FormattedSpan { range: 6..11, style: Style::Emphasis }, + FormattedSpan { range: 6..11, style: Style::Link } + ], + links: std::vec![(6..11, "https://example.com".into())] + }] + ); + + assert_eq!( + StyledText::parse("hello world").unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![FormattedSpan { range: 0..11, style: Style::Underline },], + links: std::vec![] + }] + ); + + assert_eq!( + StyledText::parse(r#"hello world"#).unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![FormattedSpan { + range: 0..11, + style: Style::Color(crate::Color::from_rgb_u8(0, 0, 255)) + },], + links: std::vec![] + }] + ); + + assert_eq!( + StyledText::parse(r#"hello world"#).unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![ + FormattedSpan { + range: 0..11, + style: Style::Color(crate::Color::from_rgb_u8(255, 0, 0)) + }, + FormattedSpan { range: 0..11, style: Style::Underline }, + ], + links: std::vec![] + }] + ); +} diff --git a/internal/core/rtti.rs b/internal/core/rtti.rs index 7d6fb159250..b9db495a0c3 100644 --- a/internal/core/rtti.rs +++ b/internal/core/rtti.rs @@ -54,6 +54,7 @@ macro_rules! declare_ValueType_2 { crate::items::MenuEntry, crate::items::DropEvent, crate::model::ModelRc, + crate::api::StyledText, $(crate::items::$Name,)* ]; }; diff --git a/internal/interpreter/api.rs b/internal/interpreter/api.rs index 76aa1a3251f..c13025ffa46 100644 --- a/internal/interpreter/api.rs +++ b/internal/interpreter/api.rs @@ -63,6 +63,8 @@ pub enum ValueType { Brush, /// Correspond to `image` type in .slint. Image, + /// Correspond to `styled-text` type in .slint. + StyledText, /// The type is not a public type but something internal. #[doc(hidden)] Other = -1, @@ -87,6 +89,7 @@ impl From for ValueType { LangType::Struct { .. } => Self::Struct, LangType::Void => Self::Void, LangType::Image => Self::Image, + LangType::StyledText => Self::StyledText, _ => Self::Other, } } @@ -140,6 +143,8 @@ pub enum Value { #[doc(hidden)] /// Correspond to the `component-factory` type in .slint ComponentFactory(ComponentFactory) = 12, + /// Correspond to the `styled-text` type in .slint + StyledText(i_slint_core::api::StyledText) = 13, } impl Value { @@ -185,6 +190,9 @@ impl PartialEq for Value { Value::ComponentFactory(lhs) => { matches!(other, Value::ComponentFactory(rhs) if lhs == rhs) } + Value::StyledText(lhs) => { + matches!(other, Value::StyledText(rhs) if lhs == rhs) + } } } } @@ -209,6 +217,7 @@ impl std::fmt::Debug for Value { Value::EnumerationValue(n, v) => write!(f, "Value::EnumerationValue({n:?}, {v:?})"), Value::LayoutCache(v) => write!(f, "Value::LayoutCache({v:?})"), Value::ComponentFactory(factory) => write!(f, "Value::ComponentFactory({factory:?})"), + Value::StyledText(text) => write!(f, "Value::StyledText({text:?})"), } } } @@ -251,6 +260,7 @@ declare_value_conversion!(PathData => [PathData]); declare_value_conversion!(EasingCurve => [i_slint_core::animations::EasingCurve]); declare_value_conversion!(LayoutCache => [SharedVector] ); declare_value_conversion!(ComponentFactory => [ComponentFactory] ); +declare_value_conversion!(StyledText => [i_slint_core::api::StyledText] ); /// Implement From / TryFrom for Value that convert a `struct` to/from `Value::Struct` macro_rules! declare_value_struct_conversion { diff --git a/internal/interpreter/dynamic_item_tree.rs b/internal/interpreter/dynamic_item_tree.rs index 65f92339329..4d1ec8d3d8b 100644 --- a/internal/interpreter/dynamic_item_tree.rs +++ b/internal/interpreter/dynamic_item_tree.rs @@ -14,7 +14,7 @@ use i_slint_compiler::{generator, object_tree, parser, CompilerConfiguration}; use i_slint_core::accessibility::{ AccessibilityAction, AccessibleStringProperty, SupportedAccessibilityAction, }; -use i_slint_core::api::LogicalPosition; +use i_slint_core::api::{LogicalPosition, StyledText}; use i_slint_core::component_factory::ComponentFactory; use i_slint_core::item_tree::{ IndexRange, ItemRc, ItemTree, ItemTreeNode, ItemTreeRef, ItemTreeRefPin, ItemTreeVTable, @@ -1261,7 +1261,7 @@ pub(crate) fn generate_item_tree<'id>( } Type::LayoutCache => property_info::>(), Type::Function { .. } | Type::Callback { .. } => return None, - + Type::StyledText => property_info::(), // These can't be used in properties Type::Invalid | Type::Void diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index f60ab172d38..6e22a66de40 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -1732,6 +1732,7 @@ fn check_value_type(value: &mut Value, ty: &Type) -> bool { } Type::LayoutCache => matches!(value, Value::LayoutCache(_)), Type::ComponentFactory => matches!(value, Value::ComponentFactory(_)), + Type::StyledText => matches!(value, Value::StyledText(_)), } } @@ -2032,6 +2033,7 @@ pub fn default_value_for_type(ty: &Type) -> Value { | Type::Function { .. } => { panic!("There can't be such property") } + Type::StyledText => Value::StyledText(Default::default()), } } From 1bf27cda6d67a9223829b49baf73a55162c5626c Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 20 Nov 2025 11:27:08 +0100 Subject: [PATCH 02/14] Move to styled-text.rs --- internal/core/api.rs | 534 +------------------------------ internal/core/api/styled_text.rs | 533 ++++++++++++++++++++++++++++++ 2 files changed, 536 insertions(+), 531 deletions(-) create mode 100644 internal/core/api/styled_text.rs diff --git a/internal/core/api.rs b/internal/core/api.rs index 180357d540e..f9f0371619e 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -15,6 +15,9 @@ use crate::window::{WindowAdapter, WindowInner}; use alloc::boxed::Box; use alloc::string::String; +mod styled_text; +pub use styled_text::*; + /// A position represented in the coordinate space of logical pixels. That is the space before applying /// a display device specific scale factor. #[derive(Debug, Default, Copy, Clone, PartialEq)] @@ -1219,534 +1222,3 @@ pub fn set_xdg_app_id(app_id: impl Into) -> Result<(), PlatformErr |ctx| ctx.set_xdg_app_id(app_id.into()), ) } - -#[derive(Clone, Debug, PartialEq)] -#[allow(missing_docs)] -pub enum Style { - Emphasis, - Strong, - Strikethrough, - Code, - Link, - Underline, - Color(crate::Color), -} - -#[derive(Clone, Debug, PartialEq)] -#[allow(missing_docs)] -pub struct FormattedSpan { - pub range: core::ops::Range, - pub style: Style, -} - -#[cfg(feature = "experimental-rich-text")] -#[derive(Clone, Debug)] -enum ListItemType { - Ordered(u64), - Unordered, -} - -#[cfg(feature = "experimental-rich-text")] -#[derive(Clone, Debug, PartialEq)] -#[allow(missing_docs)] -pub struct StyledTextParagraph { - pub text: std::string::String, - pub formatting: std::vec::Vec, - pub links: std::vec::Vec<(std::ops::Range, std::string::String)>, -} - -#[cfg(feature = "experimental-rich-text")] -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum StyledTextError<'a> { - #[error("Spans are unbalanced: stack already empty when popped")] - Pop, - #[error("Spans are unbalanced: stack contained items at end of function")] - NotEmpty, - #[error("Paragraph not started")] - ParagraphNotStarted, - #[error("Unimplemented: {:?}", .0)] - UnimplementedTag(pulldown_cmark::Tag<'a>), - #[error("Unimplemented: {:?}", .0)] - UnimplementedEvent(pulldown_cmark::Event<'a>), - #[error("Unimplemented: {}", .0)] - UnimplementedHtmlEvent(std::string::String), - #[error("Unimplemented html tag: {}", .0)] - UnimplementedHtmlTag(std::string::String), - #[error("Unexpected {} attribute in html {}", .0, .1)] - UnexpectedAttribute(std::string::String, std::string::String), - #[error("Missing color attribute in html {}", .0)] - MissingColor(std::string::String), - #[error("Closing html tag doesn't match the opening tag. Expected {}, got {}", .0, .1)] - ClosingTagMismatch(&'a str, std::string::String), -} - -/// Internal styled text type -#[derive(Debug, PartialEq, Clone, Default)] -#[allow(missing_docs)] -pub struct StyledText { - #[cfg(feature = "experimental-rich-text")] - pub paragraphs: std::vec::Vec, -} - -#[cfg(feature = "experimental-rich-text")] -#[allow(missing_docs)] -impl StyledText { - fn begin_paragraph(&mut self, indentation: u32, list_item_type: Option) { - let mut text = std::string::String::with_capacity(indentation as usize * 4); - for _ in 0..indentation { - text.push_str(" "); - } - match list_item_type { - Some(ListItemType::Unordered) => { - if indentation % 3 == 0 { - text.push_str("• ") - } else if indentation % 3 == 1 { - text.push_str("◦ ") - } else { - text.push_str("▪ ") - } - } - Some(ListItemType::Ordered(num)) => text.push_str(&std::format!("{}. ", num)), - None => {} - }; - self.paragraphs.push(StyledTextParagraph { - text, - formatting: Default::default(), - links: Default::default(), - }); - } - - pub fn parse(string: &str) -> Result> { - let parser = - pulldown_cmark::Parser::new_ext(string, pulldown_cmark::Options::ENABLE_STRIKETHROUGH); - - let mut styled_text = StyledText::default(); - let mut list_state_stack: std::vec::Vec> = std::vec::Vec::new(); - let mut style_stack = std::vec::Vec::new(); - let mut current_url = None; - - for event in parser { - let indentation = list_state_stack.len().saturating_sub(1) as _; - - match event { - pulldown_cmark::Event::SoftBreak | pulldown_cmark::Event::HardBreak => { - styled_text.begin_paragraph(indentation, None); - } - pulldown_cmark::Event::End(pulldown_cmark::TagEnd::List(_)) => { - if list_state_stack.pop().is_none() { - return Err(StyledTextError::Pop); - } - } - pulldown_cmark::Event::End( - pulldown_cmark::TagEnd::Paragraph | pulldown_cmark::TagEnd::Item, - ) => {} - pulldown_cmark::Event::Start(tag) => { - let style = match tag { - pulldown_cmark::Tag::Paragraph => { - styled_text.begin_paragraph(indentation, None); - continue; - } - pulldown_cmark::Tag::Item => { - styled_text.begin_paragraph( - indentation, - Some(match list_state_stack.last().copied() { - Some(Some(index)) => ListItemType::Ordered(index), - _ => ListItemType::Unordered, - }), - ); - if let Some(state) = list_state_stack.last_mut() { - *state = state.map(|state| state + 1); - } - continue; - } - pulldown_cmark::Tag::List(index) => { - list_state_stack.push(index); - continue; - } - pulldown_cmark::Tag::Strong => Style::Strong, - pulldown_cmark::Tag::Emphasis => Style::Emphasis, - pulldown_cmark::Tag::Strikethrough => Style::Strikethrough, - pulldown_cmark::Tag::Link { dest_url, .. } => { - current_url = Some(dest_url); - Style::Link - } - - pulldown_cmark::Tag::Heading { .. } - | pulldown_cmark::Tag::Image { .. } - | pulldown_cmark::Tag::DefinitionList - | pulldown_cmark::Tag::DefinitionListTitle - | pulldown_cmark::Tag::DefinitionListDefinition - | pulldown_cmark::Tag::TableHead - | pulldown_cmark::Tag::TableRow - | pulldown_cmark::Tag::TableCell - | pulldown_cmark::Tag::HtmlBlock - | pulldown_cmark::Tag::Superscript - | pulldown_cmark::Tag::Subscript - | pulldown_cmark::Tag::Table(_) - | pulldown_cmark::Tag::MetadataBlock(_) - | pulldown_cmark::Tag::BlockQuote(_) - | pulldown_cmark::Tag::CodeBlock(_) - | pulldown_cmark::Tag::FootnoteDefinition(_) => { - return Err(StyledTextError::UnimplementedTag(tag)); - } - }; - - style_stack.push(( - style, - styled_text - .paragraphs - .last() - .ok_or(StyledTextError::ParagraphNotStarted)? - .text - .len(), - )); - } - pulldown_cmark::Event::Text(text) => { - styled_text - .paragraphs - .last_mut() - .ok_or(StyledTextError::ParagraphNotStarted)? - .text - .push_str(&text); - } - pulldown_cmark::Event::End(_) => { - let (style, start) = if let Some(value) = style_stack.pop() { - value - } else { - return Err(StyledTextError::Pop); - }; - - let paragraph = styled_text - .paragraphs - .last_mut() - .ok_or(StyledTextError::ParagraphNotStarted)?; - let end = paragraph.text.len(); - - if let Some(url) = current_url.take() { - paragraph.links.push((start..end, url.into())); - } - - paragraph.formatting.push(FormattedSpan { range: start..end, style }); - } - pulldown_cmark::Event::Code(text) => { - let paragraph = styled_text - .paragraphs - .last_mut() - .ok_or(StyledTextError::ParagraphNotStarted)?; - let start = paragraph.text.len(); - paragraph.text.push_str(&text); - paragraph.formatting.push(FormattedSpan { - range: start..paragraph.text.len(), - style: Style::Code, - }); - } - pulldown_cmark::Event::InlineHtml(html) => { - if html.starts_with(" "", - Style::Underline => "", - other => std::unreachable!( - "Got unexpected closing style {:?} with html {}. This error should have been caught earlier.", - other, - html - ), - }; - - if (&*html) != expected_tag { - return Err(StyledTextError::ClosingTagMismatch( - expected_tag, - (&*html).into(), - )); - } - - let paragraph = styled_text - .paragraphs - .last_mut() - .ok_or(StyledTextError::ParagraphNotStarted)?; - let end = paragraph.text.len(); - paragraph.formatting.push(FormattedSpan { range: start..end, style }); - } else { - let mut expecting_color_attribute = false; - - for token in htmlparser::Tokenizer::from(&*html) { - match token { - Ok(htmlparser::Token::ElementStart { local: tag_type, .. }) => { - match &*tag_type { - "u" => { - style_stack.push(( - Style::Underline, - styled_text - .paragraphs - .last() - .ok_or(StyledTextError::ParagraphNotStarted)? - .text - .len(), - )); - } - "font" => { - expecting_color_attribute = true; - } - _ => { - return Err(StyledTextError::UnimplementedHtmlTag( - (&*tag_type).into(), - )); - } - } - } - Ok(htmlparser::Token::Attribute { - local: key, - value: Some(value), - .. - }) => match &*key { - "color" => { - if !expecting_color_attribute { - return Err(StyledTextError::UnexpectedAttribute( - (&*key).into(), - (&*html).into(), - )); - } - expecting_color_attribute = false; - - let value = - i_slint_common::color_parsing::parse_color_literal( - &*value, - ) - .or_else(|| { - i_slint_common::color_parsing::named_colors() - .get(&*value) - .copied() - }) - .expect("invalid color value"); - - style_stack.push(( - Style::Color(crate::Color::from_argb_encoded(value)), - styled_text - .paragraphs - .last() - .ok_or(StyledTextError::ParagraphNotStarted)? - .text - .len(), - )); - } - _ => { - return Err(StyledTextError::UnexpectedAttribute( - (&*key).into(), - (&*html).into(), - )); - } - }, - Ok(htmlparser::Token::ElementEnd { .. }) => {} - _ => { - return Err(StyledTextError::UnimplementedHtmlEvent( - std::format!("{:?}", token), - )); - } - } - } - - if expecting_color_attribute { - return Err(StyledTextError::MissingColor((&*html).into())); - } - } - } - pulldown_cmark::Event::Rule - | pulldown_cmark::Event::TaskListMarker(_) - | pulldown_cmark::Event::FootnoteReference(_) - | pulldown_cmark::Event::InlineMath(_) - | pulldown_cmark::Event::DisplayMath(_) - | pulldown_cmark::Event::Html(_) => { - return Err(StyledTextError::UnimplementedEvent(event)); - } - } - } - - if !style_stack.is_empty() { - return Err(StyledTextError::NotEmpty); - } - - Ok(styled_text) - } -} - -#[cfg(feature = "experimental-rich-text")] -#[test] -fn markdown_parsing() { - assert_eq!( - StyledText::parse("hello *world*").unwrap().paragraphs, - [StyledTextParagraph { - text: "hello world".into(), - formatting: std::vec![FormattedSpan { range: 6..11, style: Style::Emphasis }], - links: std::vec![] - }] - ); - - assert_eq!( - StyledText::parse( - " -- line 1 -- line 2 - " - ) - .unwrap() - .paragraphs, - [ - StyledTextParagraph { - text: "• line 1".into(), - formatting: std::vec![], - links: std::vec![] - }, - StyledTextParagraph { - text: "• line 2".into(), - formatting: std::vec![], - links: std::vec![] - } - ] - ); - - assert_eq!( - StyledText::parse( - " -1. a -2. b -4. c - " - ) - .unwrap() - .paragraphs, - [ - StyledTextParagraph { - text: "1. a".into(), - formatting: std::vec![], - links: std::vec![] - }, - StyledTextParagraph { - text: "2. b".into(), - formatting: std::vec![], - links: std::vec![] - }, - StyledTextParagraph { - text: "3. c".into(), - formatting: std::vec![], - links: std::vec![] - } - ] - ); - - assert_eq!( - StyledText::parse( - " -Normal _italic_ **strong** ~~strikethrough~~ `code` -new *line* -" - ) - .unwrap() - .paragraphs, - [ - StyledTextParagraph { - text: "Normal italic strong strikethrough code".into(), - formatting: std::vec![ - FormattedSpan { range: 7..13, style: Style::Emphasis }, - FormattedSpan { range: 14..20, style: Style::Strong }, - FormattedSpan { range: 21..34, style: Style::Strikethrough }, - FormattedSpan { range: 35..39, style: Style::Code } - ], - links: std::vec![] - }, - StyledTextParagraph { - text: "new line".into(), - formatting: std::vec![FormattedSpan { range: 4..8, style: Style::Emphasis },], - links: std::vec![] - } - ] - ); - - assert_eq!( - StyledText::parse( - " -- root - - child - - grandchild - - great grandchild -" - ) - .unwrap() - .paragraphs, - [ - StyledTextParagraph { - text: "• root".into(), - formatting: std::vec![], - links: std::vec![] - }, - StyledTextParagraph { - text: " ◦ child".into(), - formatting: std::vec![], - links: std::vec![] - }, - StyledTextParagraph { - text: " ▪ grandchild".into(), - formatting: std::vec![], - links: std::vec![] - }, - StyledTextParagraph { - text: " • great grandchild".into(), - formatting: std::vec![], - links: std::vec![] - }, - ] - ); - - assert_eq!( - StyledText::parse("hello [*world*](https://example.com)").unwrap().paragraphs, - [StyledTextParagraph { - text: "hello world".into(), - formatting: std::vec![ - FormattedSpan { range: 6..11, style: Style::Emphasis }, - FormattedSpan { range: 6..11, style: Style::Link } - ], - links: std::vec![(6..11, "https://example.com".into())] - }] - ); - - assert_eq!( - StyledText::parse("hello world").unwrap().paragraphs, - [StyledTextParagraph { - text: "hello world".into(), - formatting: std::vec![FormattedSpan { range: 0..11, style: Style::Underline },], - links: std::vec![] - }] - ); - - assert_eq!( - StyledText::parse(r#"hello world"#).unwrap().paragraphs, - [StyledTextParagraph { - text: "hello world".into(), - formatting: std::vec![FormattedSpan { - range: 0..11, - style: Style::Color(crate::Color::from_rgb_u8(0, 0, 255)) - },], - links: std::vec![] - }] - ); - - assert_eq!( - StyledText::parse(r#"hello world"#).unwrap().paragraphs, - [StyledTextParagraph { - text: "hello world".into(), - formatting: std::vec![ - FormattedSpan { - range: 0..11, - style: Style::Color(crate::Color::from_rgb_u8(255, 0, 0)) - }, - FormattedSpan { range: 0..11, style: Style::Underline }, - ], - links: std::vec![] - }] - ); -} diff --git a/internal/core/api/styled_text.rs b/internal/core/api/styled_text.rs new file mode 100644 index 00000000000..46d7ce1779b --- /dev/null +++ b/internal/core/api/styled_text.rs @@ -0,0 +1,533 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +#[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] +pub enum Style { + Emphasis, + Strong, + Strikethrough, + Code, + Link, + Underline, + Color(crate::Color), +} + +#[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] +pub struct FormattedSpan { + pub range: core::ops::Range, + pub style: Style, +} + +#[cfg(feature = "experimental-rich-text")] +#[derive(Clone, Debug)] +enum ListItemType { + Ordered(u64), + Unordered, +} + +#[cfg(feature = "experimental-rich-text")] +#[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] +pub struct StyledTextParagraph { + pub text: std::string::String, + pub formatting: std::vec::Vec, + pub links: std::vec::Vec<(std::ops::Range, std::string::String)>, +} + +#[cfg(feature = "experimental-rich-text")] +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum StyledTextError<'a> { + #[error("Spans are unbalanced: stack already empty when popped")] + Pop, + #[error("Spans are unbalanced: stack contained items at end of function")] + NotEmpty, + #[error("Paragraph not started")] + ParagraphNotStarted, + #[error("Unimplemented: {:?}", .0)] + UnimplementedTag(pulldown_cmark::Tag<'a>), + #[error("Unimplemented: {:?}", .0)] + UnimplementedEvent(pulldown_cmark::Event<'a>), + #[error("Unimplemented: {}", .0)] + UnimplementedHtmlEvent(std::string::String), + #[error("Unimplemented html tag: {}", .0)] + UnimplementedHtmlTag(std::string::String), + #[error("Unexpected {} attribute in html {}", .0, .1)] + UnexpectedAttribute(std::string::String, std::string::String), + #[error("Missing color attribute in html {}", .0)] + MissingColor(std::string::String), + #[error("Closing html tag doesn't match the opening tag. Expected {}, got {}", .0, .1)] + ClosingTagMismatch(&'a str, std::string::String), +} + +/// Internal styled text type +#[derive(Debug, PartialEq, Clone, Default)] +#[allow(missing_docs)] +pub struct StyledText { + #[cfg(feature = "experimental-rich-text")] + pub paragraphs: std::vec::Vec, +} + +#[cfg(feature = "experimental-rich-text")] +#[allow(missing_docs)] +impl StyledText { + fn begin_paragraph(&mut self, indentation: u32, list_item_type: Option) { + let mut text = std::string::String::with_capacity(indentation as usize * 4); + for _ in 0..indentation { + text.push_str(" "); + } + match list_item_type { + Some(ListItemType::Unordered) => { + if indentation % 3 == 0 { + text.push_str("• ") + } else if indentation % 3 == 1 { + text.push_str("◦ ") + } else { + text.push_str("▪ ") + } + } + Some(ListItemType::Ordered(num)) => text.push_str(&std::format!("{}. ", num)), + None => {} + }; + self.paragraphs.push(StyledTextParagraph { + text, + formatting: Default::default(), + links: Default::default(), + }); + } + + pub fn parse(string: &str) -> Result> { + let parser = + pulldown_cmark::Parser::new_ext(string, pulldown_cmark::Options::ENABLE_STRIKETHROUGH); + + let mut styled_text = StyledText::default(); + let mut list_state_stack: std::vec::Vec> = std::vec::Vec::new(); + let mut style_stack = std::vec::Vec::new(); + let mut current_url = None; + + for event in parser { + let indentation = list_state_stack.len().saturating_sub(1) as _; + + match event { + pulldown_cmark::Event::SoftBreak | pulldown_cmark::Event::HardBreak => { + styled_text.begin_paragraph(indentation, None); + } + pulldown_cmark::Event::End(pulldown_cmark::TagEnd::List(_)) => { + if list_state_stack.pop().is_none() { + return Err(StyledTextError::Pop); + } + } + pulldown_cmark::Event::End( + pulldown_cmark::TagEnd::Paragraph | pulldown_cmark::TagEnd::Item, + ) => {} + pulldown_cmark::Event::Start(tag) => { + let style = match tag { + pulldown_cmark::Tag::Paragraph => { + styled_text.begin_paragraph(indentation, None); + continue; + } + pulldown_cmark::Tag::Item => { + styled_text.begin_paragraph( + indentation, + Some(match list_state_stack.last().copied() { + Some(Some(index)) => ListItemType::Ordered(index), + _ => ListItemType::Unordered, + }), + ); + if let Some(state) = list_state_stack.last_mut() { + *state = state.map(|state| state + 1); + } + continue; + } + pulldown_cmark::Tag::List(index) => { + list_state_stack.push(index); + continue; + } + pulldown_cmark::Tag::Strong => Style::Strong, + pulldown_cmark::Tag::Emphasis => Style::Emphasis, + pulldown_cmark::Tag::Strikethrough => Style::Strikethrough, + pulldown_cmark::Tag::Link { dest_url, .. } => { + current_url = Some(dest_url); + Style::Link + } + + pulldown_cmark::Tag::Heading { .. } + | pulldown_cmark::Tag::Image { .. } + | pulldown_cmark::Tag::DefinitionList + | pulldown_cmark::Tag::DefinitionListTitle + | pulldown_cmark::Tag::DefinitionListDefinition + | pulldown_cmark::Tag::TableHead + | pulldown_cmark::Tag::TableRow + | pulldown_cmark::Tag::TableCell + | pulldown_cmark::Tag::HtmlBlock + | pulldown_cmark::Tag::Superscript + | pulldown_cmark::Tag::Subscript + | pulldown_cmark::Tag::Table(_) + | pulldown_cmark::Tag::MetadataBlock(_) + | pulldown_cmark::Tag::BlockQuote(_) + | pulldown_cmark::Tag::CodeBlock(_) + | pulldown_cmark::Tag::FootnoteDefinition(_) => { + return Err(StyledTextError::UnimplementedTag(tag)); + } + }; + + style_stack.push(( + style, + styled_text + .paragraphs + .last() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .len(), + )); + } + pulldown_cmark::Event::Text(text) => { + styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .push_str(&text); + } + pulldown_cmark::Event::End(_) => { + let (style, start) = if let Some(value) = style_stack.pop() { + value + } else { + return Err(StyledTextError::Pop); + }; + + let paragraph = styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)?; + let end = paragraph.text.len(); + + if let Some(url) = current_url.take() { + paragraph.links.push((start..end, url.into())); + } + + paragraph.formatting.push(FormattedSpan { range: start..end, style }); + } + pulldown_cmark::Event::Code(text) => { + let paragraph = styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)?; + let start = paragraph.text.len(); + paragraph.text.push_str(&text); + paragraph.formatting.push(FormattedSpan { + range: start..paragraph.text.len(), + style: Style::Code, + }); + } + pulldown_cmark::Event::InlineHtml(html) => { + if html.starts_with(" "", + Style::Underline => "", + other => std::unreachable!( + "Got unexpected closing style {:?} with html {}. This error should have been caught earlier.", + other, + html + ), + }; + + if (&*html) != expected_tag { + return Err(StyledTextError::ClosingTagMismatch( + expected_tag, + (&*html).into(), + )); + } + + let paragraph = styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)?; + let end = paragraph.text.len(); + paragraph.formatting.push(FormattedSpan { range: start..end, style }); + } else { + let mut expecting_color_attribute = false; + + for token in htmlparser::Tokenizer::from(&*html) { + match token { + Ok(htmlparser::Token::ElementStart { local: tag_type, .. }) => { + match &*tag_type { + "u" => { + style_stack.push(( + Style::Underline, + styled_text + .paragraphs + .last() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .len(), + )); + } + "font" => { + expecting_color_attribute = true; + } + _ => { + return Err(StyledTextError::UnimplementedHtmlTag( + (&*tag_type).into(), + )); + } + } + } + Ok(htmlparser::Token::Attribute { + local: key, + value: Some(value), + .. + }) => match &*key { + "color" => { + if !expecting_color_attribute { + return Err(StyledTextError::UnexpectedAttribute( + (&*key).into(), + (&*html).into(), + )); + } + expecting_color_attribute = false; + + let value = + i_slint_common::color_parsing::parse_color_literal( + &*value, + ) + .or_else(|| { + i_slint_common::color_parsing::named_colors() + .get(&*value) + .copied() + }) + .expect("invalid color value"); + + style_stack.push(( + Style::Color(crate::Color::from_argb_encoded(value)), + styled_text + .paragraphs + .last() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .len(), + )); + } + _ => { + return Err(StyledTextError::UnexpectedAttribute( + (&*key).into(), + (&*html).into(), + )); + } + }, + Ok(htmlparser::Token::ElementEnd { .. }) => {} + _ => { + return Err(StyledTextError::UnimplementedHtmlEvent( + std::format!("{:?}", token), + )); + } + } + } + + if expecting_color_attribute { + return Err(StyledTextError::MissingColor((&*html).into())); + } + } + } + pulldown_cmark::Event::Rule + | pulldown_cmark::Event::TaskListMarker(_) + | pulldown_cmark::Event::FootnoteReference(_) + | pulldown_cmark::Event::InlineMath(_) + | pulldown_cmark::Event::DisplayMath(_) + | pulldown_cmark::Event::Html(_) => { + return Err(StyledTextError::UnimplementedEvent(event)); + } + } + } + + if !style_stack.is_empty() { + return Err(StyledTextError::NotEmpty); + } + + Ok(styled_text) + } +} + +#[cfg(feature = "experimental-rich-text")] +#[test] +fn markdown_parsing() { + assert_eq!( + StyledText::parse("hello *world*").unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![FormattedSpan { range: 6..11, style: Style::Emphasis }], + links: std::vec![] + }] + ); + + assert_eq!( + StyledText::parse( + " +- line 1 +- line 2 + " + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "• line 1".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: "• line 2".into(), + formatting: std::vec![], + links: std::vec![] + } + ] + ); + + assert_eq!( + StyledText::parse( + " +1. a +2. b +4. c + " + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "1. a".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: "2. b".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: "3. c".into(), + formatting: std::vec![], + links: std::vec![] + } + ] + ); + + assert_eq!( + StyledText::parse( + " +Normal _italic_ **strong** ~~strikethrough~~ `code` +new *line* +" + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "Normal italic strong strikethrough code".into(), + formatting: std::vec![ + FormattedSpan { range: 7..13, style: Style::Emphasis }, + FormattedSpan { range: 14..20, style: Style::Strong }, + FormattedSpan { range: 21..34, style: Style::Strikethrough }, + FormattedSpan { range: 35..39, style: Style::Code } + ], + links: std::vec![] + }, + StyledTextParagraph { + text: "new line".into(), + formatting: std::vec![FormattedSpan { range: 4..8, style: Style::Emphasis },], + links: std::vec![] + } + ] + ); + + assert_eq!( + StyledText::parse( + " +- root + - child + - grandchild + - great grandchild +" + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "• root".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: " ◦ child".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: " ▪ grandchild".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: " • great grandchild".into(), + formatting: std::vec![], + links: std::vec![] + }, + ] + ); + + assert_eq!( + StyledText::parse("hello [*world*](https://example.com)").unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![ + FormattedSpan { range: 6..11, style: Style::Emphasis }, + FormattedSpan { range: 6..11, style: Style::Link } + ], + links: std::vec![(6..11, "https://example.com".into())] + }] + ); + + assert_eq!( + StyledText::parse("hello world").unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![FormattedSpan { range: 0..11, style: Style::Underline },], + links: std::vec![] + }] + ); + + assert_eq!( + StyledText::parse(r#"hello world"#).unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![FormattedSpan { + range: 0..11, + style: Style::Color(crate::Color::from_rgb_u8(0, 0, 255)) + },], + links: std::vec![] + }] + ); + + assert_eq!( + StyledText::parse(r#"hello world"#).unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![ + FormattedSpan { + range: 0..11, + style: Style::Color(crate::Color::from_rgb_u8(255, 0, 0)) + }, + FormattedSpan { range: 0..11, style: Style::Underline }, + ], + links: std::vec![] + }] + ); +} From 1d52442975bdea68b6e3c706def567ce34a5d91b Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 20 Nov 2025 12:01:51 +0100 Subject: [PATCH 03/14] Apply suggestions --- internal/core/api.rs | 2 ++ internal/core/api/styled_text.rs | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/internal/core/api.rs b/internal/core/api.rs index f9f0371619e..8c1b6c7fa17 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -15,7 +15,9 @@ use crate::window::{WindowAdapter, WindowInner}; use alloc::boxed::Box; use alloc::string::String; +#[cfg(feature = "experimental-rich-text")] mod styled_text; +#[cfg(feature = "experimental-rich-text")] pub use styled_text::*; /// A position represented in the coordinate space of logical pixels. That is the space before applying diff --git a/internal/core/api/styled_text.rs b/internal/core/api/styled_text.rs index 46d7ce1779b..769eca2609f 100644 --- a/internal/core/api/styled_text.rs +++ b/internal/core/api/styled_text.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 #[derive(Clone, Debug, PartialEq)] +/// Styles that can be applied to text spans #[allow(missing_docs)] pub enum Style { Emphasis, @@ -14,31 +15,34 @@ pub enum Style { } #[derive(Clone, Debug, PartialEq)] -#[allow(missing_docs)] +/// A style and a text span pub struct FormattedSpan { + /// Span of text to style pub range: core::ops::Range, + /// The style to apply pub style: Style, } -#[cfg(feature = "experimental-rich-text")] #[derive(Clone, Debug)] enum ListItemType { Ordered(u64), Unordered, } -#[cfg(feature = "experimental-rich-text")] +/// A section of styled text, split up by a linebreak #[derive(Clone, Debug, PartialEq)] -#[allow(missing_docs)] pub struct StyledTextParagraph { + /// The raw paragraph text pub text: std::string::String, + /// Formatting styles and spans pub formatting: std::vec::Vec, + /// Locations of clickable links within the paragraph pub links: std::vec::Vec<(std::ops::Range, std::string::String)>, } -#[cfg(feature = "experimental-rich-text")] #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] +#[non_exhaustive] pub enum StyledTextError<'a> { #[error("Spans are unbalanced: stack already empty when popped")] Pop, @@ -64,14 +68,11 @@ pub enum StyledTextError<'a> { /// Internal styled text type #[derive(Debug, PartialEq, Clone, Default)] -#[allow(missing_docs)] pub struct StyledText { - #[cfg(feature = "experimental-rich-text")] + /// Paragraphs of styled text pub paragraphs: std::vec::Vec, } -#[cfg(feature = "experimental-rich-text")] -#[allow(missing_docs)] impl StyledText { fn begin_paragraph(&mut self, indentation: u32, list_item_type: Option) { let mut text = std::string::String::with_capacity(indentation as usize * 4); @@ -98,6 +99,7 @@ impl StyledText { }); } + /// Parse a markdown string as styled text pub fn parse(string: &str) -> Result> { let parser = pulldown_cmark::Parser::new_ext(string, pulldown_cmark::Options::ENABLE_STRIKETHROUGH); @@ -356,7 +358,6 @@ impl StyledText { } } -#[cfg(feature = "experimental-rich-text")] #[test] fn markdown_parsing() { assert_eq!( From 0ca15e26c049f038669421c618c17d6ceef6bc4c Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 20 Nov 2025 13:19:37 +0100 Subject: [PATCH 04/14] Don't gate around experimental-rich-text feature --- internal/core/Cargo.toml | 10 +++++----- internal/core/api.rs | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index 214183ff57b..fcc116a2890 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -68,7 +68,7 @@ raw-window-handle-06 = ["dep:raw-window-handle-06"] experimental = [] -experimental-rich-text = ["dep:pulldown-cmark", "dep:htmlparser", "dep:thiserror", "i-slint-common/color-parsing"] +experimental-rich-text = [] unstable-wgpu-26 = ["dep:wgpu-26"] unstable-wgpu-27 = ["dep:wgpu-27"] @@ -81,7 +81,7 @@ default = ["std", "unicode"] shared-parley = ["shared-fontique", "dep:parley", "dep:skrifa"] [dependencies] -i-slint-common = { workspace = true, features = ["default"] } +i-slint-common = { workspace = true, features = ["default", "color-parsing"] } i-slint-core-macros = { workspace = true, features = ["default"] } const-field-offset = { version = "0.1.5", path = "../../helper_crates/const-field-offset" } @@ -115,7 +115,7 @@ bytemuck = { workspace = true, optional = true, features = ["derive"] } zeno = { version = "0.3.3", optional = true, default-features = false, features = ["eval"] } sys-locale = { version = "0.3.2", optional = true, features = ["js"] } parley = { version = "0.6.0", optional = true } -pulldown-cmark = { version = "0.13.0", optional = true } +pulldown-cmark = "0.13.0" image = { workspace = true, optional = true, default-features = false } clru = { workspace = true, optional = true } @@ -136,8 +136,8 @@ wgpu-27 = { workspace = true, optional = true } tr = { workspace = true, optional = true } webbrowser = { version = "1.0.6", optional = true } -htmlparser = { version = "0.2.1", optional = true } -thiserror = { version = "2.0.17", optional = true } +htmlparser = "0.2.1" +thiserror = "2.0.17" [target.'cfg(target_family = "unix")'.dependencies] gettext-rs = { version = "0.7.1", optional = true, features = ["gettext-system"] } diff --git a/internal/core/api.rs b/internal/core/api.rs index 8c1b6c7fa17..f9f0371619e 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -15,9 +15,7 @@ use crate::window::{WindowAdapter, WindowInner}; use alloc::boxed::Box; use alloc::string::String; -#[cfg(feature = "experimental-rich-text")] mod styled_text; -#[cfg(feature = "experimental-rich-text")] pub use styled_text::*; /// A position represented in the coordinate space of logical pixels. That is the space before applying From 235cc2551350b3d4df067f87451d7feea29743c7 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 20 Nov 2025 14:05:39 +0100 Subject: [PATCH 05/14] Gate pulldown_cmark behind std --- internal/core/Cargo.toml | 3 +- internal/core/api/styled_text.rs | 94 ++++++++++++++++---------------- 2 files changed, 50 insertions(+), 47 deletions(-) diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index fcc116a2890..59506055318 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -44,6 +44,7 @@ std = [ "dep:sys-locale", "dep:webbrowser", "shared-fontique", + "dep:pulldown-cmark" ] # Unsafe feature meaning that there is only one core running and all thread_local are static. # You can only enable this feature if you are sure that any API of this crate is only called @@ -115,7 +116,7 @@ bytemuck = { workspace = true, optional = true, features = ["derive"] } zeno = { version = "0.3.3", optional = true, default-features = false, features = ["eval"] } sys-locale = { version = "0.3.2", optional = true, features = ["js"] } parley = { version = "0.6.0", optional = true } -pulldown-cmark = "0.13.0" +pulldown-cmark = { version = "0.13.0", optional = true } image = { workspace = true, optional = true, default-features = false } clru = { workspace = true, optional = true } diff --git a/internal/core/api/styled_text.rs b/internal/core/api/styled_text.rs index 769eca2609f..bc961ed21b5 100644 --- a/internal/core/api/styled_text.rs +++ b/internal/core/api/styled_text.rs @@ -33,13 +33,14 @@ enum ListItemType { #[derive(Clone, Debug, PartialEq)] pub struct StyledTextParagraph { /// The raw paragraph text - pub text: std::string::String, + pub text: alloc::string::String, /// Formatting styles and spans - pub formatting: std::vec::Vec, + pub formatting: alloc::vec::Vec, /// Locations of clickable links within the paragraph - pub links: std::vec::Vec<(std::ops::Range, std::string::String)>, + pub links: alloc::vec::Vec<(core::ops::Range, alloc::string::String)>, } +#[cfg(feature = "std")] #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] #[non_exhaustive] @@ -55,27 +56,27 @@ pub enum StyledTextError<'a> { #[error("Unimplemented: {:?}", .0)] UnimplementedEvent(pulldown_cmark::Event<'a>), #[error("Unimplemented: {}", .0)] - UnimplementedHtmlEvent(std::string::String), + UnimplementedHtmlEvent(alloc::string::String), #[error("Unimplemented html tag: {}", .0)] - UnimplementedHtmlTag(std::string::String), + UnimplementedHtmlTag(alloc::string::String), #[error("Unexpected {} attribute in html {}", .0, .1)] - UnexpectedAttribute(std::string::String, std::string::String), + UnexpectedAttribute(alloc::string::String, alloc::string::String), #[error("Missing color attribute in html {}", .0)] - MissingColor(std::string::String), + MissingColor(alloc::string::String), #[error("Closing html tag doesn't match the opening tag. Expected {}, got {}", .0, .1)] - ClosingTagMismatch(&'a str, std::string::String), + ClosingTagMismatch(&'a str, alloc::string::String), } /// Internal styled text type #[derive(Debug, PartialEq, Clone, Default)] pub struct StyledText { /// Paragraphs of styled text - pub paragraphs: std::vec::Vec, + pub paragraphs: alloc::vec::Vec, } impl StyledText { fn begin_paragraph(&mut self, indentation: u32, list_item_type: Option) { - let mut text = std::string::String::with_capacity(indentation as usize * 4); + let mut text = alloc::string::String::with_capacity(indentation as usize * 4); for _ in 0..indentation { text.push_str(" "); } @@ -89,7 +90,7 @@ impl StyledText { text.push_str("▪ ") } } - Some(ListItemType::Ordered(num)) => text.push_str(&std::format!("{}. ", num)), + Some(ListItemType::Ordered(num)) => text.push_str(&alloc::format!("{}. ", num)), None => {} }; self.paragraphs.push(StyledTextParagraph { @@ -100,13 +101,14 @@ impl StyledText { } /// Parse a markdown string as styled text + #[cfg(feature = "std")] pub fn parse(string: &str) -> Result> { let parser = pulldown_cmark::Parser::new_ext(string, pulldown_cmark::Options::ENABLE_STRIKETHROUGH); let mut styled_text = StyledText::default(); - let mut list_state_stack: std::vec::Vec> = std::vec::Vec::new(); - let mut style_stack = std::vec::Vec::new(); + let mut list_state_stack: alloc::vec::Vec> = alloc::vec::Vec::new(); + let mut style_stack = alloc::vec::Vec::new(); let mut current_url = None; for event in parser { @@ -328,7 +330,7 @@ impl StyledText { Ok(htmlparser::Token::ElementEnd { .. }) => {} _ => { return Err(StyledTextError::UnimplementedHtmlEvent( - std::format!("{:?}", token), + alloc::format!("{:?}", token), )); } } @@ -364,8 +366,8 @@ fn markdown_parsing() { StyledText::parse("hello *world*").unwrap().paragraphs, [StyledTextParagraph { text: "hello world".into(), - formatting: std::vec![FormattedSpan { range: 6..11, style: Style::Emphasis }], - links: std::vec![] + formatting: alloc::vec![FormattedSpan { range: 6..11, style: Style::Emphasis }], + links: alloc::vec![] }] ); @@ -381,13 +383,13 @@ fn markdown_parsing() { [ StyledTextParagraph { text: "• line 1".into(), - formatting: std::vec![], - links: std::vec![] + formatting: alloc::vec![], + links: alloc::vec![] }, StyledTextParagraph { text: "• line 2".into(), - formatting: std::vec![], - links: std::vec![] + formatting: alloc::vec![], + links: alloc::vec![] } ] ); @@ -405,18 +407,18 @@ fn markdown_parsing() { [ StyledTextParagraph { text: "1. a".into(), - formatting: std::vec![], - links: std::vec![] + formatting: alloc::vec![], + links: alloc::vec![] }, StyledTextParagraph { text: "2. b".into(), - formatting: std::vec![], - links: std::vec![] + formatting: alloc::vec![], + links: alloc::vec![] }, StyledTextParagraph { text: "3. c".into(), - formatting: std::vec![], - links: std::vec![] + formatting: alloc::vec![], + links: alloc::vec![] } ] ); @@ -433,18 +435,18 @@ new *line* [ StyledTextParagraph { text: "Normal italic strong strikethrough code".into(), - formatting: std::vec![ + formatting: alloc::vec![ FormattedSpan { range: 7..13, style: Style::Emphasis }, FormattedSpan { range: 14..20, style: Style::Strong }, FormattedSpan { range: 21..34, style: Style::Strikethrough }, FormattedSpan { range: 35..39, style: Style::Code } ], - links: std::vec![] + links: alloc::vec![] }, StyledTextParagraph { text: "new line".into(), - formatting: std::vec![FormattedSpan { range: 4..8, style: Style::Emphasis },], - links: std::vec![] + formatting: alloc::vec![FormattedSpan { range: 4..8, style: Style::Emphasis },], + links: alloc::vec![] } ] ); @@ -463,23 +465,23 @@ new *line* [ StyledTextParagraph { text: "• root".into(), - formatting: std::vec![], - links: std::vec![] + formatting: alloc::vec![], + links: alloc::vec![] }, StyledTextParagraph { text: " ◦ child".into(), - formatting: std::vec![], - links: std::vec![] + formatting: alloc::vec![], + links: alloc::vec![] }, StyledTextParagraph { text: " ▪ grandchild".into(), - formatting: std::vec![], - links: std::vec![] + formatting: alloc::vec![], + links: alloc::vec![] }, StyledTextParagraph { text: " • great grandchild".into(), - formatting: std::vec![], - links: std::vec![] + formatting: alloc::vec![], + links: alloc::vec![] }, ] ); @@ -488,11 +490,11 @@ new *line* StyledText::parse("hello [*world*](https://example.com)").unwrap().paragraphs, [StyledTextParagraph { text: "hello world".into(), - formatting: std::vec![ + formatting: alloc::vec![ FormattedSpan { range: 6..11, style: Style::Emphasis }, FormattedSpan { range: 6..11, style: Style::Link } ], - links: std::vec![(6..11, "https://example.com".into())] + links: alloc::vec![(6..11, "https://example.com".into())] }] ); @@ -500,8 +502,8 @@ new *line* StyledText::parse("hello world").unwrap().paragraphs, [StyledTextParagraph { text: "hello world".into(), - formatting: std::vec![FormattedSpan { range: 0..11, style: Style::Underline },], - links: std::vec![] + formatting: alloc::vec![FormattedSpan { range: 0..11, style: Style::Underline },], + links: alloc::vec![] }] ); @@ -509,11 +511,11 @@ new *line* StyledText::parse(r#"hello world"#).unwrap().paragraphs, [StyledTextParagraph { text: "hello world".into(), - formatting: std::vec![FormattedSpan { + formatting: alloc::vec![FormattedSpan { range: 0..11, style: Style::Color(crate::Color::from_rgb_u8(0, 0, 255)) },], - links: std::vec![] + links: alloc::vec![] }] ); @@ -521,14 +523,14 @@ new *line* StyledText::parse(r#"hello world"#).unwrap().paragraphs, [StyledTextParagraph { text: "hello world".into(), - formatting: std::vec![ + formatting: alloc::vec![ FormattedSpan { range: 0..11, style: Style::Color(crate::Color::from_rgb_u8(255, 0, 0)) }, FormattedSpan { range: 0..11, style: Style::Underline }, ], - links: std::vec![] + links: alloc::vec![] }] ); } From 729b16aa7b17ea82678c2263ce1c50dfbd572f74 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:21:21 +0000 Subject: [PATCH 06/14] [autofix.ci] apply automated fixes --- internal/core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index 59506055318..cfafa338aa7 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -44,7 +44,7 @@ std = [ "dep:sys-locale", "dep:webbrowser", "shared-fontique", - "dep:pulldown-cmark" + "dep:pulldown-cmark", ] # Unsafe feature meaning that there is only one core running and all thread_local are static. # You can only enable this feature if you are sure that any API of this crate is only called From 8093aa7a0f9b903016add8ab027bba1e60d2efe1 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 20 Nov 2025 15:21:33 +0100 Subject: [PATCH 07/14] Make thiserror optional --- internal/core/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index cfafa338aa7..86d4da467d7 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -45,6 +45,7 @@ std = [ "dep:webbrowser", "shared-fontique", "dep:pulldown-cmark", + "dep:thiserror", ] # Unsafe feature meaning that there is only one core running and all thread_local are static. # You can only enable this feature if you are sure that any API of this crate is only called @@ -138,7 +139,7 @@ tr = { workspace = true, optional = true } webbrowser = { version = "1.0.6", optional = true } htmlparser = "0.2.1" -thiserror = "2.0.17" +thiserror = { version = "2.0.17", optional = true } [target.'cfg(target_family = "unix")'.dependencies] gettext-rs = { version = "0.7.1", optional = true, features = ["gettext-system"] } From 53ca5790c2deb9d395691bbc89cd7e7fcbecfb68 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 20 Nov 2025 15:37:44 +0100 Subject: [PATCH 08/14] Make htmlparser optional --- internal/core/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index 86d4da467d7..37b74675954 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -46,6 +46,7 @@ std = [ "shared-fontique", "dep:pulldown-cmark", "dep:thiserror", + "dep:htmlparser", ] # Unsafe feature meaning that there is only one core running and all thread_local are static. # You can only enable this feature if you are sure that any API of this crate is only called @@ -138,7 +139,7 @@ wgpu-27 = { workspace = true, optional = true } tr = { workspace = true, optional = true } webbrowser = { version = "1.0.6", optional = true } -htmlparser = "0.2.1" +htmlparser = { version = "0.2.1", optional = true } thiserror = { version = "2.0.17", optional = true } [target.'cfg(target_family = "unix")'.dependencies] From 2f696da03f67369d71027dbe193ca0ae67c1dc7d Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 20 Nov 2025 16:09:42 +0100 Subject: [PATCH 09/14] Gate color-parsing feature --- internal/core/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index 37b74675954..44dde2d8042 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -47,6 +47,7 @@ std = [ "dep:pulldown-cmark", "dep:thiserror", "dep:htmlparser", + "i-slint-common/color-parsing" ] # Unsafe feature meaning that there is only one core running and all thread_local are static. # You can only enable this feature if you are sure that any API of this crate is only called @@ -84,7 +85,7 @@ default = ["std", "unicode"] shared-parley = ["shared-fontique", "dep:parley", "dep:skrifa"] [dependencies] -i-slint-common = { workspace = true, features = ["default", "color-parsing"] } +i-slint-common = { workspace = true, features = ["default"] } i-slint-core-macros = { workspace = true, features = ["default"] } const-field-offset = { version = "0.1.5", path = "../../helper_crates/const-field-offset" } From a6244712311dda09f06287c9d9e9b1fa4522fd80 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:25:42 +0000 Subject: [PATCH 10/14] [autofix.ci] apply automated fixes --- internal/core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index 44dde2d8042..29cfe4e3ff7 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -47,7 +47,7 @@ std = [ "dep:pulldown-cmark", "dep:thiserror", "dep:htmlparser", - "i-slint-common/color-parsing" + "i-slint-common/color-parsing", ] # Unsafe feature meaning that there is only one core running and all thread_local are static. # You can only enable this feature if you are sure that any API of this crate is only called From b10caccb3652343a1aa905ed393bd75dc992c4e6 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 20 Nov 2025 16:48:17 +0100 Subject: [PATCH 11/14] Change flags --- internal/core/api/styled_text.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/core/api/styled_text.rs b/internal/core/api/styled_text.rs index bc961ed21b5..c26afca7546 100644 --- a/internal/core/api/styled_text.rs +++ b/internal/core/api/styled_text.rs @@ -23,6 +23,7 @@ pub struct FormattedSpan { pub style: Style, } +#[cfg(feature = "std")] #[derive(Clone, Debug)] enum ListItemType { Ordered(u64), @@ -74,6 +75,7 @@ pub struct StyledText { pub paragraphs: alloc::vec::Vec, } +#[cfg(feature = "std")] impl StyledText { fn begin_paragraph(&mut self, indentation: u32, list_item_type: Option) { let mut text = alloc::string::String::with_capacity(indentation as usize * 4); @@ -101,7 +103,6 @@ impl StyledText { } /// Parse a markdown string as styled text - #[cfg(feature = "std")] pub fn parse(string: &str) -> Result> { let parser = pulldown_cmark::Parser::new_ext(string, pulldown_cmark::Options::ENABLE_STRIKETHROUGH); From 374b6d93f71527298ac9ed8c3127c552124b1a0f Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Fri, 21 Nov 2025 12:58:09 +0100 Subject: [PATCH 12/14] Change to pub(crate) --- internal/core/api/styled_text.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/core/api/styled_text.rs b/internal/core/api/styled_text.rs index c26afca7546..6e13bf9975d 100644 --- a/internal/core/api/styled_text.rs +++ b/internal/core/api/styled_text.rs @@ -3,8 +3,8 @@ #[derive(Clone, Debug, PartialEq)] /// Styles that can be applied to text spans -#[allow(missing_docs)] -pub enum Style { +#[allow(missing_docs,dead_code)] +pub(crate) enum Style { Emphasis, Strong, Strikethrough, @@ -16,11 +16,11 @@ pub enum Style { #[derive(Clone, Debug, PartialEq)] /// A style and a text span -pub struct FormattedSpan { +pub(crate) struct FormattedSpan { /// Span of text to style - pub range: core::ops::Range, + pub(crate) range: core::ops::Range, /// The style to apply - pub style: Style, + pub(crate) style: Style, } #[cfg(feature = "std")] @@ -32,13 +32,13 @@ enum ListItemType { /// A section of styled text, split up by a linebreak #[derive(Clone, Debug, PartialEq)] -pub struct StyledTextParagraph { +pub(crate) struct StyledTextParagraph { /// The raw paragraph text - pub text: alloc::string::String, + pub(crate) text: alloc::string::String, /// Formatting styles and spans - pub formatting: alloc::vec::Vec, + pub(crate) formatting: alloc::vec::Vec, /// Locations of clickable links within the paragraph - pub links: alloc::vec::Vec<(core::ops::Range, alloc::string::String)>, + pub(crate) links: alloc::vec::Vec<(core::ops::Range, alloc::string::String)>, } #[cfg(feature = "std")] @@ -72,7 +72,7 @@ pub enum StyledTextError<'a> { #[derive(Debug, PartialEq, Clone, Default)] pub struct StyledText { /// Paragraphs of styled text - pub paragraphs: alloc::vec::Vec, + pub(crate) paragraphs: alloc::vec::Vec, } #[cfg(feature = "std")] From 7cbfe8c3052abc69d80fb0d3efb25347ee970d37 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:02:34 +0000 Subject: [PATCH 13/14] [autofix.ci] apply automated fixes --- internal/core/api/styled_text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/api/styled_text.rs b/internal/core/api/styled_text.rs index 6e13bf9975d..e01cd97a67c 100644 --- a/internal/core/api/styled_text.rs +++ b/internal/core/api/styled_text.rs @@ -3,7 +3,7 @@ #[derive(Clone, Debug, PartialEq)] /// Styles that can be applied to text spans -#[allow(missing_docs,dead_code)] +#[allow(missing_docs, dead_code)] pub(crate) enum Style { Emphasis, Strong, From e4c37e219e7f473643f34673d94d8274742c5d16 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Fri, 21 Nov 2025 14:27:18 +0100 Subject: [PATCH 14/14] Better docs --- internal/core/api/styled_text.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/core/api/styled_text.rs b/internal/core/api/styled_text.rs index e01cd97a67c..fda3ad3bd1b 100644 --- a/internal/core/api/styled_text.rs +++ b/internal/core/api/styled_text.rs @@ -41,34 +41,44 @@ pub(crate) struct StyledTextParagraph { pub(crate) links: alloc::vec::Vec<(core::ops::Range, alloc::string::String)>, } +/// Error type returned by `StyledText::parse` #[cfg(feature = "std")] #[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] #[non_exhaustive] pub enum StyledTextError<'a> { + /// Spans are unbalanced: stack already empty when popped #[error("Spans are unbalanced: stack already empty when popped")] Pop, + /// Spans are unbalanced: stack contained items at end of function #[error("Spans are unbalanced: stack contained items at end of function")] NotEmpty, + /// Paragraph not started #[error("Paragraph not started")] ParagraphNotStarted, + /// Unimplemented markdown tag #[error("Unimplemented: {:?}", .0)] UnimplementedTag(pulldown_cmark::Tag<'a>), + /// Unimplemented markdown event #[error("Unimplemented: {:?}", .0)] UnimplementedEvent(pulldown_cmark::Event<'a>), + /// Unimplemented html event #[error("Unimplemented: {}", .0)] UnimplementedHtmlEvent(alloc::string::String), + /// Unimplemented html tag #[error("Unimplemented html tag: {}", .0)] UnimplementedHtmlTag(alloc::string::String), + /// Unimplemented html attribute #[error("Unexpected {} attribute in html {}", .0, .1)] UnexpectedAttribute(alloc::string::String, alloc::string::String), + /// Missing color attribute in html #[error("Missing color attribute in html {}", .0)] MissingColor(alloc::string::String), + /// Closing html tag doesn't match the opening tag #[error("Closing html tag doesn't match the opening tag. Expected {}, got {}", .0, .1)] ClosingTagMismatch(&'a str, alloc::string::String), } -/// Internal styled text type +/// Styled text that has been parsed and seperated into paragraphs #[derive(Debug, PartialEq, Clone, Default)] pub struct StyledText { /// Paragraphs of styled text