diff --git a/docs/src/features/code/latex.md b/docs/src/features/code/latex.md index 8d6fb543..91f9dc54 100644 --- a/docs/src/features/code/latex.md +++ b/docs/src/features/code/latex.md @@ -86,3 +86,5 @@ typst: horizontal_margin: 2 vertical_margin: 2 ``` + +You can also make use of an alpha channel for typst colours, e.g. `background: ff0000aa`. diff --git a/src/export/html.rs b/src/export/html.rs index 8b0f6f62..a0583f78 100644 --- a/src/export/html.rs +++ b/src/export/html.rs @@ -84,6 +84,7 @@ pub(crate) fn color_to_html(color: &Color) -> String { Color::White => "#ffffff".into(), Color::Grey => "#808080".into(), Color::Rgb { r, g, b } => format!("#{r:02x}{g:02x}{b:02x}"), + Color::Rgba { r, g, b, a } => format!("#{r:02x}{g:02x}{b:02x}{a:02x}"), } } @@ -105,6 +106,8 @@ mod test { )] #[case::foreground_color(TextStyle::default().fg_color(Color::new(1,2,3)), "color: #010203")] #[case::background_color(TextStyle::default().bg_color(Color::new(1,2,3)), "background-color: #010203")] + #[case::foreground_color_alpha(TextStyle::default().fg_color(Color::Rgba {r: 1, g: 2, b: 3, a:4 }), "color: #01020304")] + #[case::background_color_alpha(TextStyle::default().bg_color(Color::Rgba { r: 1, g: 2, b: 3, a: 4 }), "background-color: #01020304")] #[case::font_size(TextStyle::default().size(3), "font-size: 6px")] fn html_text(#[case] style: TextStyle, #[case] expected_style: &str) { let html_text = HtmlText::new("", &style, FontSize::Pixels(2)); diff --git a/src/markdown/text_style.rs b/src/markdown/text_style.rs index f9124a3c..569fb00f 100644 --- a/src/markdown/text_style.rs +++ b/src/markdown/text_style.rs @@ -304,6 +304,7 @@ pub(crate) enum Color { White, Grey, Rgb { r: u8, g: u8, b: u8 }, + Rgba { r: u8, g: u8, b: u8, a: u8 }, } impl Color { @@ -346,6 +347,15 @@ impl Color { pub(crate) fn as_rgb(&self) -> Option<(u8, u8, u8)> { match self { Self::Rgb { r, g, b } => Some((*r, *g, *b)), + Self::Rgba { r, g, b, .. } => Some((*r, *g, *b)), + _ => None, + } + } + + pub(crate) fn as_rgba(&self) -> Option<(u8, u8, u8, u8)> { + match self { + Self::Rgb { r, g, b } => Some((*r, *g, *b, u8::MAX)), + Self::Rgba { r, g, b, a } => Some((*r, *g, *b, *a)), _ => None, } } @@ -387,6 +397,7 @@ impl From for crossterm::style::Color { Color::White => C::White, Color::Grey => C::Grey, Color::Rgb { r, g, b } => C::Rgb { r, g, b }, + Color::Rgba { r, g, b, .. } => C::Rgb { r, g, b }, } } } @@ -518,4 +529,20 @@ mod tests { let attrs: Vec<_> = style.iter_attributes().collect(); assert_eq!(attrs, expected); } + + #[rstest] + #[case::constant(Color::Green, None)] + #[case::rgb(Color::Rgb { r: 12, g: 38, b: 42 }, Some((12, 38, 42)))] + #[case::rgba(Color::Rgba { r: 12, g: 38, b: 42, a: 72 }, Some((12, 38, 42)))] + fn as_rgb(#[case] color: Color, #[case] expected: Option<(u8, u8, u8)>) { + assert_eq!(color.as_rgb(), expected); + } + + #[rstest] + #[case::constant(Color::Green, None)] + #[case::rgb(Color::Rgb { r: 12, g: 38, b: 42 }, Some((12, 38, 42, 255)))] + #[case::rgba(Color::Rgba { r: 12, g: 38, b: 42, a: 72 }, Some((12, 38, 42, 72)))] + fn as_rgba(#[case] color: Color, #[case] expected: Option<(u8, u8, u8, u8)>) { + assert_eq!(color.as_rgba(), expected); + } } diff --git a/src/theme/raw.rs b/src/theme/raw.rs index 224171ea..bc615f91 100644 --- a/src/theme/raw.rs +++ b/src/theme/raw.rs @@ -943,12 +943,18 @@ impl FromStr for RawColor { // Fallback to hex-encoded rgb _ => { let hex = match input.len() { - 6 => input.to_string(), 3 => input.chars().flat_map(|c| [c, c]).collect::(), + 6 => input.to_string(), + 8 => input.to_string(), len => return Err(ParseColorError::InvalidHexLength(len)), }; - let values = <[u8; 3]>::from_hex(hex)?; - Color::Rgb { r: values[0], g: values[1], b: values[2] }.into() + if hex.len() == 6 { + let values = <[u8; 3]>::from_hex(hex)?; + Color::Rgb { r: values[0], g: values[1], b: values[2] }.into() + } else { + let values = <[u8; 4]>::from_hex(hex)?; + Color::Rgba { r: values[0], g: values[1], b: values[2], a: values[3] }.into() + } } }; Ok(output) @@ -960,6 +966,7 @@ impl fmt::Display for RawColor { use Color::*; match self { Self::Color(Rgb { r, g, b }) => write!(f, "{}", hex::encode([*r, *g, *b])), + Self::Color(Rgba { r, g, b, a }) => write!(f, "{}", hex::encode([*r, *g, *b, *a])), Self::Color(Black) => write!(f, "black"), Self::Color(White) => write!(f, "white"), Self::Color(Grey) => write!(f, "grey"), @@ -1058,6 +1065,9 @@ mod test { let short_color: RawColor = "ded".parse().unwrap(); assert_eq!(short_color.to_string(), "ddeedd"); + + let rgba_color: RawColor = "beefff42".parse().unwrap(); + assert_eq!(rgba_color.to_string(), "beefff42"); } #[rstest] diff --git a/src/third_party.rs b/src/third_party.rs index ab23011a..8a948120 100644 --- a/src/third_party.rs +++ b/src/third_party.rs @@ -299,8 +299,8 @@ impl Worker { } fn as_typst_color(color: &Color) -> Result { - match color.as_rgb() { - Some((r, g, b)) => Ok(format!("rgb(\"#{r:02x}{g:02x}{b:02x}\")")), + match color.as_rgba() { + Some((r, g, b, a)) => Ok(format!("rgb(\"#{r:02x}{g:02x}{b:02x}{a:02x}\")")), None => Err(ThirdPartyRenderError::UnsupportedColor(RawColor::from(*color).to_string())), } } @@ -429,3 +429,20 @@ impl Pollable for OperationPollable { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn as_typst_color() { + let rgb = Worker::as_typst_color(&Color::Rgb { r: 19, g: 57, b: 72 }); + assert!(rgb.is_ok()); + assert_eq!("rgb(\"#133948ff\")".to_string(), rgb.unwrap()); + let rgba = Worker::as_typst_color(&Color::Rgba { r: 102, g: 2, b: 37, a: 24 }); + assert!(rgba.is_ok()); + assert_eq!("rgb(\"#66022518\")".to_string(), rgba.unwrap()); + let err = Worker::as_typst_color(&Color::Red); + assert!(err.is_err()); + } +}