online Minecraft written book viewer

feat(text): improve a lot

kokirigla.de b0a5c7d1 33e72147

verified
+201 -61
+165
nara_text/src/color.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 4 + #[serde(rename_all = "snake_case")] 5 + pub enum NamedColor { 6 + Black, 7 + DarkBlue, 8 + DarkGreen, 9 + DarkAqua, 10 + DarkRed, 11 + DarkPurple, 12 + Gold, 13 + Gray, 14 + DarkGray, 15 + Blue, 16 + Green, 17 + Aqua, 18 + Red, 19 + LightPurple, 20 + Yellow, 21 + White, 22 + } 23 + 24 + impl NamedColor { 25 + pub fn as_hex(&self) -> [u8; 3] { 26 + match self { 27 + NamedColor::Black => [0x00, 0x00, 0x00], 28 + NamedColor::DarkBlue => [0x00, 0x00, 0xAA], 29 + NamedColor::DarkGreen => [0x00, 0xAA, 0x00], 30 + NamedColor::DarkAqua => [0x00, 0xAA, 0xAA], 31 + NamedColor::DarkRed => [0xAA, 0x00, 0x00], 32 + NamedColor::DarkPurple => [0xAA, 0x00, 0xAA], 33 + NamedColor::Gold => [0xFF, 0xAA, 0x00], 34 + NamedColor::Gray => [0xAA, 0xAA, 0xAA], 35 + NamedColor::DarkGray => [0x55, 0x55, 0x55], 36 + NamedColor::Blue => [0x55, 0x55, 0xFF], 37 + NamedColor::Green => [0x55, 0xFF, 0x55], 38 + NamedColor::Aqua => [0x55, 0xFF, 0xFF], 39 + NamedColor::Red => [0xFF, 0x55, 0x55], 40 + NamedColor::LightPurple => [0xFF, 0x55, 0xFF], 41 + NamedColor::Yellow => [0xFF, 0xFF, 0x55], 42 + NamedColor::White => [0xFF, 0xFF, 0xFF], 43 + } 44 + } 45 + 46 + pub fn as_hex_string(&self) -> String { 47 + hex_bytes_to_string(self.as_hex()) 48 + } 49 + 50 + pub fn from_hex(hex: [u8; 3]) -> Option<Self> { 51 + match hex { 52 + [0x00, 0x00, 0x00] => Some(NamedColor::Black), 53 + [0x00, 0x00, 0xAA] => Some(NamedColor::DarkBlue), 54 + [0x00, 0xAA, 0x00] => Some(NamedColor::DarkGreen), 55 + [0x00, 0xAA, 0xAA] => Some(NamedColor::DarkAqua), 56 + [0xAA, 0x00, 0x00] => Some(NamedColor::DarkRed), 57 + [0xAA, 0x00, 0xAA] => Some(NamedColor::DarkPurple), 58 + [0xFF, 0xAA, 0x00] => Some(NamedColor::Gold), 59 + [0xAA, 0xAA, 0xAA] => Some(NamedColor::Gray), 60 + [0x55, 0x55, 0x55] => Some(NamedColor::DarkGray), 61 + [0x55, 0x55, 0xFF] => Some(NamedColor::Blue), 62 + [0x55, 0xFF, 0x55] => Some(NamedColor::Green), 63 + [0x55, 0xFF, 0xFF] => Some(NamedColor::Aqua), 64 + [0xFF, 0x55, 0x55] => Some(NamedColor::Red), 65 + [0xFF, 0x55, 0xFF] => Some(NamedColor::LightPurple), 66 + [0xFF, 0xFF, 0x55] => Some(NamedColor::Yellow), 67 + [0xFF, 0xFF, 0xFF] => Some(NamedColor::White), 68 + _ => None, 69 + } 70 + } 71 + 72 + pub fn from_name(s: &str) -> Option<Self> { 73 + let normalized = s.trim().to_ascii_lowercase().replace([' ', '-'], "_"); 74 + 75 + match normalized.as_str() { 76 + "black" => Some(NamedColor::Black), 77 + "dark_blue" => Some(NamedColor::DarkBlue), 78 + "dark_green" => Some(NamedColor::DarkGreen), 79 + "dark_aqua" => Some(NamedColor::DarkAqua), 80 + "dark_red" => Some(NamedColor::DarkRed), 81 + "dark_purple" => Some(NamedColor::DarkPurple), 82 + "gold" => Some(NamedColor::Gold), 83 + "gray" | "grey" => Some(NamedColor::Gray), 84 + "dark_gray" | "dark_grey" => Some(NamedColor::DarkGray), 85 + "blue" => Some(NamedColor::Blue), 86 + "green" => Some(NamedColor::Green), 87 + "aqua" => Some(NamedColor::Aqua), 88 + "red" => Some(NamedColor::Red), 89 + "light_purple" => Some(NamedColor::LightPurple), 90 + "yellow" => Some(NamedColor::Yellow), 91 + "white" => Some(NamedColor::White), 92 + _ => None, 93 + } 94 + } 95 + } 96 + 97 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 98 + #[serde(try_from = "String", into = "String")] 99 + pub enum Color { 100 + Named(NamedColor), 101 + Hex(String), 102 + } 103 + 104 + impl Color { 105 + pub fn as_hex(&self) -> Result<[u8; 3], String> { 106 + match self { 107 + Color::Named(named) => Ok(named.as_hex()), 108 + Color::Hex(hex) => hex_string_to_bytes(hex), 109 + } 110 + } 111 + 112 + pub fn as_hex_string(&self) -> String { 113 + match self { 114 + Color::Named(named) => named.as_hex_string(), 115 + Color::Hex(hex) => match hex_string_to_bytes(hex) { 116 + Ok(bytes) => hex_bytes_to_string(bytes), 117 + Err(_) => hex.clone(), 118 + }, 119 + } 120 + } 121 + } 122 + 123 + impl TryFrom<String> for Color { 124 + type Error = String; 125 + 126 + fn try_from(value: String) -> Result<Self, Self::Error> { 127 + if let Some(named) = NamedColor::from_name(&value) { 128 + return Ok(Color::Named(named)); 129 + } 130 + 131 + let bytes = hex_string_to_bytes(&value)?; 132 + if let Some(named) = NamedColor::from_hex(bytes) { 133 + Ok(Color::Named(named)) 134 + } else { 135 + Ok(Color::Hex(hex_bytes_to_string(bytes))) 136 + } 137 + } 138 + } 139 + 140 + impl From<Color> for String { 141 + fn from(value: Color) -> Self { 142 + value.as_hex_string() 143 + } 144 + } 145 + 146 + fn hex_string_to_bytes(s: &str) -> Result<[u8; 3], String> { 147 + let s = s.trim().strip_prefix('#').unwrap_or(s.trim()); 148 + 149 + if s.len() != 6 { 150 + return Err(format!("expected 6 hex characters, got {}", s.len())); 151 + } 152 + 153 + let r = u8::from_str_radix(&s[0..2], 16) 154 + .map_err(|_| "invalid red channel".to_string())?; 155 + let g = u8::from_str_radix(&s[2..4], 16) 156 + .map_err(|_| "invalid green channel".to_string())?; 157 + let b = u8::from_str_radix(&s[4..6], 16) 158 + .map_err(|_| "invalid blue channel".to_string())?; 159 + 160 + Ok([r, g, b]) 161 + } 162 + 163 + fn hex_bytes_to_string(bytes: [u8; 3]) -> String { 164 + format!("#{:02X}{:02X}{:02X}", bytes[0], bytes[1], bytes[2]) 165 + }
+36 -61
nara_text/src/lib.rs
··· 1 - #[derive(Debug)] 1 + use serde::{Deserialize, Serialize}; 2 + 3 + pub mod color; 4 + 5 + #[derive(Debug, Serialize, Deserialize)] 6 + #[serde(untagged)] 2 7 pub enum Component { 3 8 String(String), 4 9 Object(ComponentObject), 5 10 Array(Vec<Component>), 6 11 } 7 12 8 - #[derive(Debug, Clone, Copy)] 9 - pub enum NamedColor { 10 - Black, 11 - DarkBlue, 12 - DarkGreen, 13 - DarkAqua, 14 - DarkRed, 15 - DarkPurple, 16 - Gold, 17 - Gray, 18 - DarkGray, 19 - Blue, 20 - Green, 21 - Aqua, 22 - Red, 23 - LightPurple, 24 - Yellow, 25 - White, 13 + #[serde_with::skip_serializing_none] 14 + #[derive(Debug, Serialize, Deserialize)] 15 + pub struct ComponentObject { 16 + #[serde(flatten)] 17 + text: Option<TextComponent>, 18 + #[serde(flatten)] 19 + translation: Option<TranslationComponent>, 20 + #[serde(flatten)] 21 + formatting: ComponentFormatting, 22 + #[serde(default, rename = "extra", skip_serializing_if = "Vec::is_empty")] 23 + children: Vec<Component>, 26 24 } 27 25 28 - impl NamedColor { 29 - fn as_hex_string(&self) -> String { 30 - match self { 31 - NamedColor::Black => "#000000".to_string(), 32 - NamedColor::DarkBlue => "#0000AA".to_string(), 33 - NamedColor::DarkGreen => "#00AA00".to_string(), 34 - NamedColor::DarkAqua => "#00AAAA".to_string(), 35 - NamedColor::DarkRed => "#AA0000".to_string(), 36 - NamedColor::DarkPurple => "#AA00AA".to_string(), 37 - NamedColor::Gold => "#FFAA00".to_string(), 38 - NamedColor::Gray => "#AAAAAA".to_string(), 39 - NamedColor::DarkGray => "#555555".to_string(), 40 - NamedColor::Blue => "#5555FF".to_string(), 41 - NamedColor::Green => "#55FF55".to_string(), 42 - NamedColor::Aqua => "#55FFFF".to_string(), 43 - NamedColor::Red => "#FF5555".to_string(), 44 - NamedColor::LightPurple => "#FF55FF".to_string(), 45 - NamedColor::Yellow => "#FFFF55".to_string(), 46 - NamedColor::White => "#FFFFFF".to_string(), 47 - } 48 - } 49 - } 50 - 51 - #[derive(Debug, Clone)] 52 - pub enum Color { 53 - Named(NamedColor), 54 - Hex(String), 55 - } 56 - 57 - impl Color { 58 - fn as_hex_string(&self) -> String { 59 - match self { 60 - Color::Named(named_color) => todo!(), 61 - Color::Hex(hex) => hex.clone(), 62 - } 63 - } 64 - 65 - #[derive(Debug)] 66 - pub struct ComponentObject { 67 - extra: Option<Vec<Component>>, 68 - color: Option<Color>, 69 - font: Option<String>, 26 + #[serde_with::skip_serializing_none] 27 + #[derive(Debug, Serialize, Deserialize)] 28 + pub struct ComponentFormatting { 29 + color: Option<color::Color>, 30 + font: Option<String>, // todo(kokiriglade): an Identifier struct? or is that too over-enginereed? 70 31 bold: Option<bool>, 71 32 italic: Option<bool>, 72 33 underlined: Option<bool>, 73 34 strikethrough: Option<bool>, 74 35 obfuscated: Option<bool>, 75 36 } 37 + 38 + #[derive(Debug, Serialize, Deserialize)] 39 + pub struct TextComponent { 40 + text: String, 41 + } 42 + 43 + #[serde_with::skip_serializing_none] 44 + #[derive(Debug, Serialize, Deserialize)] 45 + pub struct TranslationComponent { 46 + translate: String, 47 + fallback: Option<String>, 48 + #[serde(default, skip_serializing_if = "Vec::is_empty")] 49 + with: Vec<Component>, 50 + }