pub enum Component {
String(String),
Array(Vec<Component>),
Object(Box<ComponentObject>),
}Expand description
Represents a Minecraft text component. Allows de/serialization using Serde with JSON.
Variants§
String(String)
Simple string component (shorthand for {text: "value"})
Array(Vec<Component>)
Array of components (shorthand for first component with extras)
Object(Box<ComponentObject>)
Full component object with properties
Implementations§
Source§impl Component
impl Component
Sourcepub fn text(text: impl AsRef<str>) -> Self
pub fn text(text: impl AsRef<str>) -> Self
Creates a plain text component
Examples found in repository?
More examples
124 fn parse_text(&mut self) -> Result<(), MiniMessageError> {
125 let start = self.position;
126 while self.position < self.input.len() {
127 if self.starts_with('<') || (self.config.parse_legacy_colors && self.starts_with('&')) {
128 break;
129 }
130 self.position += 1;
131 }
132
133 if start < self.position {
134 let text = &self.input[start..self.position];
135 let current_style = self.current_style();
136 let mut comp = Component::text(text);
137 comp = comp.color(current_style.color.clone());
138 comp = comp.decorations(&self.collect_decorations());
139 self.component_parts.push(comp);
140 }
141 Ok(())
142 }
143
144 fn parse_tag(&mut self) -> Result<(), MiniMessageError> {
145 // Skip '<'
146 self.position += 1;
147
148 if self.starts_with('/') {
149 // Closing tag
150 self.position += 1;
151 let tag_name = self.read_tag_name()?;
152 self.handle_close_tag(&tag_name)?;
153 self.expect('>')?;
154 } else {
155 // Opening tag
156 let tag_name = self.read_tag_name()?;
157 let mut args = Vec::new();
158 let mut self_closing = false;
159
160 while self.position < self.input.len() {
161 // skip whitespace
162 self.skip_whitespace();
163 // skip colon separators
164 while self.starts_with(':') {
165 self.position += 1;
166 self.skip_whitespace();
167 }
168
169 // if we’ve hit the end of the tag, stop
170 if self.starts_with('>') || self.starts_with('/') {
171 break;
172 }
173
174 // read an argument
175 let arg = self.read_argument()?;
176 args.push(arg);
177 }
178
179 // Check for self-closing tag
180 if self.starts_with('/') {
181 self.position += 1;
182 self_closing = true;
183 }
184 self.expect('>')?;
185
186 self.handle_open_tag(&tag_name, args, self_closing)?;
187 }
188
189 Ok(())
190 }
191
192 fn read_tag_name(&mut self) -> Result<String, MiniMessageError> {
193 let start = self.position;
194 while self.position < self.input.len() {
195 let c = self.current_char();
196 if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
197 break;
198 }
199 self.position += 1;
200 }
201 if start == self.position {
202 return Err(MiniMessageError("Expected tag name".to_string()));
203 }
204 Ok(self.input[start..self.position].to_lowercase())
205 }
206
207 fn read_argument(&mut self) -> Result<String, MiniMessageError> {
208 if self.starts_with('\'') || self.starts_with('"') {
209 self.read_quoted_string()
210 } else {
211 self.read_unquoted_string()
212 }
213 }
214
215 fn read_quoted_string(&mut self) -> Result<String, MiniMessageError> {
216 let quote_char = self.current_char();
217 self.position += 1;
218
219 let mut escaped = false;
220 let mut result = String::new();
221
222 while self.position < self.input.len() {
223 let c = self.current_char();
224 if escaped {
225 result.push(c);
226 escaped = false;
227 } else if c == '\\' {
228 escaped = true;
229 } else if c == quote_char {
230 self.position += 1;
231 return Ok(result);
232 } else {
233 result.push(c);
234 }
235 self.position += 1;
236 }
237
238 Err(MiniMessageError("Unterminated quoted string".to_string()))
239 }
240
241 fn read_unquoted_string(&mut self) -> Result<String, MiniMessageError> {
242 let start = self.position;
243 while self.position < self.input.len() {
244 let c = self.current_char();
245 if c == ':' || c == '>' || c == '/' || c.is_whitespace() {
246 break;
247 }
248 self.position += 1;
249 }
250 // what?
251 // if start == self.position {
252 // return Err(MiniMessageError("Expected argument".to_string()));
253 // }
254 Ok(self.input[start..self.position].to_string())
255 }
256
257 fn handle_open_tag(
258 &mut self,
259 tag: &str,
260 args: Vec<String>,
261 self_closing: bool,
262 ) -> Result<(), MiniMessageError> {
263 match tag {
264 // Colors
265 "black" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Black)))?,
266 "dark_blue" => {
267 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkBlue)))?
268 }
269 "dark_green" => {
270 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkGreen)))?
271 }
272 "dark_aqua" => {
273 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkAqua)))?
274 }
275 "dark_red" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkRed)))?,
276 "dark_purple" => {
277 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkPurple)))?
278 }
279 "gold" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Gold)))?,
280 "gray" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Gray)))?,
281 "dark_gray" => {
282 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkGray)))?
283 }
284 "blue" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Blue)))?,
285 "green" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Green)))?,
286 "aqua" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Aqua)))?,
287 "red" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Red)))?,
288 "light_purple" => {
289 self.push_style(|s| s.color = Some(Color::Named(NamedColor::LightPurple)))?
290 }
291 "yellow" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Yellow)))?,
292 "white" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::White)))?,
293 "color" | "colour" | "c" if !args.is_empty() => {
294 if let Some(color) = args[0].parse::<Color>().ok() {
295 self.push_style(|s| s.color = Some(color))?
296 }
297 }
298
299 // Decorations
300 "bold" | "b" => self.push_style(|s| s.bold = Some(true))?,
301 "italic" | "i" | "em" => self.push_style(|s| s.italic = Some(true))?,
302 "underlined" | "u" => self.push_style(|s| s.underlined = Some(true))?,
303 "strikethrough" | "st" => self.push_style(|s| s.strikethrough = Some(true))?,
304 "obfuscated" | "obf" => self.push_style(|s| s.obfuscated = Some(true))?,
305
306 // Reset tag
307 "reset" => self.reset_style()?,
308
309 // Click events
310 "click" if args.len() >= 2 => {
311 let action = args[0].as_str();
312 let value = args[1].as_str();
313 match action {
314 "run_command" => self.push_style(|s| {
315 s.click_event = Some(ClickEvent::RunCommand {
316 command: value.to_string(),
317 })
318 })?,
319 "suggest_command" => self.push_style(|s| {
320 s.click_event = Some(ClickEvent::SuggestCommand {
321 command: value.to_string(),
322 })
323 })?,
324 "open_url" => self.push_style(|s| {
325 s.click_event = Some(ClickEvent::OpenUrl {
326 url: value.to_string(),
327 })
328 })?,
329 "copy_to_clipboard" => self.push_style(|s| {
330 s.click_event = Some(ClickEvent::CopyToClipboard {
331 value: value.to_string(),
332 })
333 })?,
334 _ => {}
335 }
336 }
337
338 // Hover events
339 "hover" if !args.is_empty() => {
340 let action = args[0].as_str();
341 if action == "show_text" && args.len() >= 2 {
342 let mut nested_parser = Parser::new(&args[1], self.config);
343 let nested = nested_parser.parse()?;
344 self.push_style(|s| {
345 s.hover_event = Some(HoverEvent::ShowText { value: nested })
346 })?;
347 }
348 }
349
350 // Newline
351 "newline" | "br" => {
352 self.component_parts.push(Component::text("\n"));
353 }
354
355 // Insertion
356 "insert" | "insertion" if !args.is_empty() => {
357 self.push_style(|s| s.insertion = Some(args[0].clone()))?
358 }
359
360 // Handle self-closing tags
361 _ if self_closing => {
362 // For self-closing tags, create an empty component with the style
363 let current_style = self.current_style();
364 let mut comp = Component::text("");
365 comp = comp.color(current_style.color.clone());
366 comp = comp.decorations(&self.collect_decorations());
367 self.component_parts.push(comp);
368 }
369
370 // Unknown tags are treated as text
371 _ => {
372 let mut tag_text = format!("<{tag}");
373 for arg in args {
374 tag_text.push(':');
375 tag_text.push_str(&arg);
376 }
377 if self_closing {
378 tag_text.push('/');
379 }
380 tag_text.push('>');
381 self.component_parts
382 .push(Component::text(tag_text).apply_fallback_style(self.current_style()));
383 }
384 }
385
386 Ok(())
387 }Sourcepub fn append_newline(self) -> Self
pub fn append_newline(self) -> Self
Appends a newline character
Sourcepub fn append_space(self) -> Self
pub fn append_space(self) -> Self
Appends a space character
Sourcepub fn to_plain_text(&self) -> Cow<'_, str>
pub fn to_plain_text(&self) -> Cow<'_, str>
Returns the “plain text” representation of this component as a Cow<str>.
This is the closest equivalent to Kyori’s plain text serializer
§Behavior by variant
Component::String: Returns a borrowed&strof the string content.Component::Object:- If the object has
textand noextrachildren, returns a borrowed&str. - If the object has
extrachildren, returns an ownedStringconcatenating the object’stext(if any) with theto_plain_textof each child.
- If the object has
Component::Array: Returns an ownedStringconcatenating theto_plain_textof each element in the array.
§Notes
This method may allocate a new String if concatenation is needed.
Use Self::get_plain_text if you only need a cheap, O(1) borrowed string from a single component.
Examples found in repository?
593 pub fn to_plain_text(&self) -> Cow<'_, str> {
594 match self {
595 Component::String(s) => Cow::Borrowed(s),
596 Component::Object(obj) => {
597 if obj.extra.is_none()
598 && let Some(text) = &obj.text
599 {
600 return Cow::Borrowed(text);
601 }
602
603 let mut result = String::new();
604 if let Some(text) = &obj.text {
605 result.push_str(text);
606 }
607 if let Some(children) = &obj.extra {
608 for child in children {
609 result.push_str(&child.to_plain_text());
610 }
611 }
612 Cow::Owned(result)
613 }
614 Component::Array(components) => {
615 let mut result = String::new();
616 for c in components {
617 result.push_str(&c.to_plain_text());
618 }
619 Cow::Owned(result)
620 }
621 }
622 }Sourcepub fn get_plain_text(&self) -> Option<&str>
pub fn get_plain_text(&self) -> Option<&str>
Returns the raw text field if this component is a string or an object with a text field.
Does not traverse children or consider other fields. Cheap, O(1) operation alternative to Self::to_plain_text.
Sourcepub fn apply_fallback_style(self, fallback: &Style) -> Self
pub fn apply_fallback_style(self, fallback: &Style) -> Self
Applies fallback styles to unset properties
Examples found in repository?
636 pub fn apply_fallback_style(self, fallback: &Style) -> Self {
637 match self {
638 Component::String(s) => {
639 let mut obj = ComponentObject {
640 content_type: Some(ContentType::Text),
641 text: Some(s),
642 ..Default::default()
643 };
644 obj.merge_style(fallback);
645 Component::Object(Box::new(obj))
646 }
647 Component::Array(vec) => Component::Array(
648 vec.into_iter()
649 .map(|c| c.apply_fallback_style(fallback))
650 .collect(),
651 ),
652 Component::Object(mut obj) => {
653 obj.merge_style(fallback);
654 if let Some(extras) = obj.extra {
655 obj.extra = Some(
656 extras
657 .into_iter()
658 .map(|c| c.apply_fallback_style(fallback))
659 .collect(),
660 );
661 }
662 Component::Object(obj)
663 }
664 }
665 }More examples
257 fn handle_open_tag(
258 &mut self,
259 tag: &str,
260 args: Vec<String>,
261 self_closing: bool,
262 ) -> Result<(), MiniMessageError> {
263 match tag {
264 // Colors
265 "black" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Black)))?,
266 "dark_blue" => {
267 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkBlue)))?
268 }
269 "dark_green" => {
270 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkGreen)))?
271 }
272 "dark_aqua" => {
273 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkAqua)))?
274 }
275 "dark_red" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkRed)))?,
276 "dark_purple" => {
277 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkPurple)))?
278 }
279 "gold" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Gold)))?,
280 "gray" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Gray)))?,
281 "dark_gray" => {
282 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkGray)))?
283 }
284 "blue" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Blue)))?,
285 "green" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Green)))?,
286 "aqua" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Aqua)))?,
287 "red" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Red)))?,
288 "light_purple" => {
289 self.push_style(|s| s.color = Some(Color::Named(NamedColor::LightPurple)))?
290 }
291 "yellow" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Yellow)))?,
292 "white" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::White)))?,
293 "color" | "colour" | "c" if !args.is_empty() => {
294 if let Some(color) = args[0].parse::<Color>().ok() {
295 self.push_style(|s| s.color = Some(color))?
296 }
297 }
298
299 // Decorations
300 "bold" | "b" => self.push_style(|s| s.bold = Some(true))?,
301 "italic" | "i" | "em" => self.push_style(|s| s.italic = Some(true))?,
302 "underlined" | "u" => self.push_style(|s| s.underlined = Some(true))?,
303 "strikethrough" | "st" => self.push_style(|s| s.strikethrough = Some(true))?,
304 "obfuscated" | "obf" => self.push_style(|s| s.obfuscated = Some(true))?,
305
306 // Reset tag
307 "reset" => self.reset_style()?,
308
309 // Click events
310 "click" if args.len() >= 2 => {
311 let action = args[0].as_str();
312 let value = args[1].as_str();
313 match action {
314 "run_command" => self.push_style(|s| {
315 s.click_event = Some(ClickEvent::RunCommand {
316 command: value.to_string(),
317 })
318 })?,
319 "suggest_command" => self.push_style(|s| {
320 s.click_event = Some(ClickEvent::SuggestCommand {
321 command: value.to_string(),
322 })
323 })?,
324 "open_url" => self.push_style(|s| {
325 s.click_event = Some(ClickEvent::OpenUrl {
326 url: value.to_string(),
327 })
328 })?,
329 "copy_to_clipboard" => self.push_style(|s| {
330 s.click_event = Some(ClickEvent::CopyToClipboard {
331 value: value.to_string(),
332 })
333 })?,
334 _ => {}
335 }
336 }
337
338 // Hover events
339 "hover" if !args.is_empty() => {
340 let action = args[0].as_str();
341 if action == "show_text" && args.len() >= 2 {
342 let mut nested_parser = Parser::new(&args[1], self.config);
343 let nested = nested_parser.parse()?;
344 self.push_style(|s| {
345 s.hover_event = Some(HoverEvent::ShowText { value: nested })
346 })?;
347 }
348 }
349
350 // Newline
351 "newline" | "br" => {
352 self.component_parts.push(Component::text("\n"));
353 }
354
355 // Insertion
356 "insert" | "insertion" if !args.is_empty() => {
357 self.push_style(|s| s.insertion = Some(args[0].clone()))?
358 }
359
360 // Handle self-closing tags
361 _ if self_closing => {
362 // For self-closing tags, create an empty component with the style
363 let current_style = self.current_style();
364 let mut comp = Component::text("");
365 comp = comp.color(current_style.color.clone());
366 comp = comp.decorations(&self.collect_decorations());
367 self.component_parts.push(comp);
368 }
369
370 // Unknown tags are treated as text
371 _ => {
372 let mut tag_text = format!("<{tag}");
373 for arg in args {
374 tag_text.push(':');
375 tag_text.push_str(&arg);
376 }
377 if self_closing {
378 tag_text.push('/');
379 }
380 tag_text.push('>');
381 self.component_parts
382 .push(Component::text(tag_text).apply_fallback_style(self.current_style()));
383 }
384 }
385
386 Ok(())
387 }Sourcepub fn color(self, color: Option<Color>) -> Self
pub fn color(self, color: Option<Color>) -> Self
Sets text color
Examples found in repository?
124 fn parse_text(&mut self) -> Result<(), MiniMessageError> {
125 let start = self.position;
126 while self.position < self.input.len() {
127 if self.starts_with('<') || (self.config.parse_legacy_colors && self.starts_with('&')) {
128 break;
129 }
130 self.position += 1;
131 }
132
133 if start < self.position {
134 let text = &self.input[start..self.position];
135 let current_style = self.current_style();
136 let mut comp = Component::text(text);
137 comp = comp.color(current_style.color.clone());
138 comp = comp.decorations(&self.collect_decorations());
139 self.component_parts.push(comp);
140 }
141 Ok(())
142 }
143
144 fn parse_tag(&mut self) -> Result<(), MiniMessageError> {
145 // Skip '<'
146 self.position += 1;
147
148 if self.starts_with('/') {
149 // Closing tag
150 self.position += 1;
151 let tag_name = self.read_tag_name()?;
152 self.handle_close_tag(&tag_name)?;
153 self.expect('>')?;
154 } else {
155 // Opening tag
156 let tag_name = self.read_tag_name()?;
157 let mut args = Vec::new();
158 let mut self_closing = false;
159
160 while self.position < self.input.len() {
161 // skip whitespace
162 self.skip_whitespace();
163 // skip colon separators
164 while self.starts_with(':') {
165 self.position += 1;
166 self.skip_whitespace();
167 }
168
169 // if we’ve hit the end of the tag, stop
170 if self.starts_with('>') || self.starts_with('/') {
171 break;
172 }
173
174 // read an argument
175 let arg = self.read_argument()?;
176 args.push(arg);
177 }
178
179 // Check for self-closing tag
180 if self.starts_with('/') {
181 self.position += 1;
182 self_closing = true;
183 }
184 self.expect('>')?;
185
186 self.handle_open_tag(&tag_name, args, self_closing)?;
187 }
188
189 Ok(())
190 }
191
192 fn read_tag_name(&mut self) -> Result<String, MiniMessageError> {
193 let start = self.position;
194 while self.position < self.input.len() {
195 let c = self.current_char();
196 if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
197 break;
198 }
199 self.position += 1;
200 }
201 if start == self.position {
202 return Err(MiniMessageError("Expected tag name".to_string()));
203 }
204 Ok(self.input[start..self.position].to_lowercase())
205 }
206
207 fn read_argument(&mut self) -> Result<String, MiniMessageError> {
208 if self.starts_with('\'') || self.starts_with('"') {
209 self.read_quoted_string()
210 } else {
211 self.read_unquoted_string()
212 }
213 }
214
215 fn read_quoted_string(&mut self) -> Result<String, MiniMessageError> {
216 let quote_char = self.current_char();
217 self.position += 1;
218
219 let mut escaped = false;
220 let mut result = String::new();
221
222 while self.position < self.input.len() {
223 let c = self.current_char();
224 if escaped {
225 result.push(c);
226 escaped = false;
227 } else if c == '\\' {
228 escaped = true;
229 } else if c == quote_char {
230 self.position += 1;
231 return Ok(result);
232 } else {
233 result.push(c);
234 }
235 self.position += 1;
236 }
237
238 Err(MiniMessageError("Unterminated quoted string".to_string()))
239 }
240
241 fn read_unquoted_string(&mut self) -> Result<String, MiniMessageError> {
242 let start = self.position;
243 while self.position < self.input.len() {
244 let c = self.current_char();
245 if c == ':' || c == '>' || c == '/' || c.is_whitespace() {
246 break;
247 }
248 self.position += 1;
249 }
250 // what?
251 // if start == self.position {
252 // return Err(MiniMessageError("Expected argument".to_string()));
253 // }
254 Ok(self.input[start..self.position].to_string())
255 }
256
257 fn handle_open_tag(
258 &mut self,
259 tag: &str,
260 args: Vec<String>,
261 self_closing: bool,
262 ) -> Result<(), MiniMessageError> {
263 match tag {
264 // Colors
265 "black" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Black)))?,
266 "dark_blue" => {
267 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkBlue)))?
268 }
269 "dark_green" => {
270 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkGreen)))?
271 }
272 "dark_aqua" => {
273 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkAqua)))?
274 }
275 "dark_red" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkRed)))?,
276 "dark_purple" => {
277 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkPurple)))?
278 }
279 "gold" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Gold)))?,
280 "gray" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Gray)))?,
281 "dark_gray" => {
282 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkGray)))?
283 }
284 "blue" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Blue)))?,
285 "green" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Green)))?,
286 "aqua" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Aqua)))?,
287 "red" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Red)))?,
288 "light_purple" => {
289 self.push_style(|s| s.color = Some(Color::Named(NamedColor::LightPurple)))?
290 }
291 "yellow" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Yellow)))?,
292 "white" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::White)))?,
293 "color" | "colour" | "c" if !args.is_empty() => {
294 if let Some(color) = args[0].parse::<Color>().ok() {
295 self.push_style(|s| s.color = Some(color))?
296 }
297 }
298
299 // Decorations
300 "bold" | "b" => self.push_style(|s| s.bold = Some(true))?,
301 "italic" | "i" | "em" => self.push_style(|s| s.italic = Some(true))?,
302 "underlined" | "u" => self.push_style(|s| s.underlined = Some(true))?,
303 "strikethrough" | "st" => self.push_style(|s| s.strikethrough = Some(true))?,
304 "obfuscated" | "obf" => self.push_style(|s| s.obfuscated = Some(true))?,
305
306 // Reset tag
307 "reset" => self.reset_style()?,
308
309 // Click events
310 "click" if args.len() >= 2 => {
311 let action = args[0].as_str();
312 let value = args[1].as_str();
313 match action {
314 "run_command" => self.push_style(|s| {
315 s.click_event = Some(ClickEvent::RunCommand {
316 command: value.to_string(),
317 })
318 })?,
319 "suggest_command" => self.push_style(|s| {
320 s.click_event = Some(ClickEvent::SuggestCommand {
321 command: value.to_string(),
322 })
323 })?,
324 "open_url" => self.push_style(|s| {
325 s.click_event = Some(ClickEvent::OpenUrl {
326 url: value.to_string(),
327 })
328 })?,
329 "copy_to_clipboard" => self.push_style(|s| {
330 s.click_event = Some(ClickEvent::CopyToClipboard {
331 value: value.to_string(),
332 })
333 })?,
334 _ => {}
335 }
336 }
337
338 // Hover events
339 "hover" if !args.is_empty() => {
340 let action = args[0].as_str();
341 if action == "show_text" && args.len() >= 2 {
342 let mut nested_parser = Parser::new(&args[1], self.config);
343 let nested = nested_parser.parse()?;
344 self.push_style(|s| {
345 s.hover_event = Some(HoverEvent::ShowText { value: nested })
346 })?;
347 }
348 }
349
350 // Newline
351 "newline" | "br" => {
352 self.component_parts.push(Component::text("\n"));
353 }
354
355 // Insertion
356 "insert" | "insertion" if !args.is_empty() => {
357 self.push_style(|s| s.insertion = Some(args[0].clone()))?
358 }
359
360 // Handle self-closing tags
361 _ if self_closing => {
362 // For self-closing tags, create an empty component with the style
363 let current_style = self.current_style();
364 let mut comp = Component::text("");
365 comp = comp.color(current_style.color.clone());
366 comp = comp.decorations(&self.collect_decorations());
367 self.component_parts.push(comp);
368 }
369
370 // Unknown tags are treated as text
371 _ => {
372 let mut tag_text = format!("<{tag}");
373 for arg in args {
374 tag_text.push(':');
375 tag_text.push_str(&arg);
376 }
377 if self_closing {
378 tag_text.push('/');
379 }
380 tag_text.push('>');
381 self.component_parts
382 .push(Component::text(tag_text).apply_fallback_style(self.current_style()));
383 }
384 }
385
386 Ok(())
387 }Sourcepub fn decoration(self, decoration: TextDecoration, state: Option<bool>) -> Self
pub fn decoration(self, decoration: TextDecoration, state: Option<bool>) -> Self
Sets text decoration state
Sourcepub fn decorations(
self,
decorations: &HashMap<TextDecoration, Option<bool>>,
) -> Self
pub fn decorations( self, decorations: &HashMap<TextDecoration, Option<bool>>, ) -> Self
Sets multiple decorations at once
Examples found in repository?
124 fn parse_text(&mut self) -> Result<(), MiniMessageError> {
125 let start = self.position;
126 while self.position < self.input.len() {
127 if self.starts_with('<') || (self.config.parse_legacy_colors && self.starts_with('&')) {
128 break;
129 }
130 self.position += 1;
131 }
132
133 if start < self.position {
134 let text = &self.input[start..self.position];
135 let current_style = self.current_style();
136 let mut comp = Component::text(text);
137 comp = comp.color(current_style.color.clone());
138 comp = comp.decorations(&self.collect_decorations());
139 self.component_parts.push(comp);
140 }
141 Ok(())
142 }
143
144 fn parse_tag(&mut self) -> Result<(), MiniMessageError> {
145 // Skip '<'
146 self.position += 1;
147
148 if self.starts_with('/') {
149 // Closing tag
150 self.position += 1;
151 let tag_name = self.read_tag_name()?;
152 self.handle_close_tag(&tag_name)?;
153 self.expect('>')?;
154 } else {
155 // Opening tag
156 let tag_name = self.read_tag_name()?;
157 let mut args = Vec::new();
158 let mut self_closing = false;
159
160 while self.position < self.input.len() {
161 // skip whitespace
162 self.skip_whitespace();
163 // skip colon separators
164 while self.starts_with(':') {
165 self.position += 1;
166 self.skip_whitespace();
167 }
168
169 // if we’ve hit the end of the tag, stop
170 if self.starts_with('>') || self.starts_with('/') {
171 break;
172 }
173
174 // read an argument
175 let arg = self.read_argument()?;
176 args.push(arg);
177 }
178
179 // Check for self-closing tag
180 if self.starts_with('/') {
181 self.position += 1;
182 self_closing = true;
183 }
184 self.expect('>')?;
185
186 self.handle_open_tag(&tag_name, args, self_closing)?;
187 }
188
189 Ok(())
190 }
191
192 fn read_tag_name(&mut self) -> Result<String, MiniMessageError> {
193 let start = self.position;
194 while self.position < self.input.len() {
195 let c = self.current_char();
196 if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
197 break;
198 }
199 self.position += 1;
200 }
201 if start == self.position {
202 return Err(MiniMessageError("Expected tag name".to_string()));
203 }
204 Ok(self.input[start..self.position].to_lowercase())
205 }
206
207 fn read_argument(&mut self) -> Result<String, MiniMessageError> {
208 if self.starts_with('\'') || self.starts_with('"') {
209 self.read_quoted_string()
210 } else {
211 self.read_unquoted_string()
212 }
213 }
214
215 fn read_quoted_string(&mut self) -> Result<String, MiniMessageError> {
216 let quote_char = self.current_char();
217 self.position += 1;
218
219 let mut escaped = false;
220 let mut result = String::new();
221
222 while self.position < self.input.len() {
223 let c = self.current_char();
224 if escaped {
225 result.push(c);
226 escaped = false;
227 } else if c == '\\' {
228 escaped = true;
229 } else if c == quote_char {
230 self.position += 1;
231 return Ok(result);
232 } else {
233 result.push(c);
234 }
235 self.position += 1;
236 }
237
238 Err(MiniMessageError("Unterminated quoted string".to_string()))
239 }
240
241 fn read_unquoted_string(&mut self) -> Result<String, MiniMessageError> {
242 let start = self.position;
243 while self.position < self.input.len() {
244 let c = self.current_char();
245 if c == ':' || c == '>' || c == '/' || c.is_whitespace() {
246 break;
247 }
248 self.position += 1;
249 }
250 // what?
251 // if start == self.position {
252 // return Err(MiniMessageError("Expected argument".to_string()));
253 // }
254 Ok(self.input[start..self.position].to_string())
255 }
256
257 fn handle_open_tag(
258 &mut self,
259 tag: &str,
260 args: Vec<String>,
261 self_closing: bool,
262 ) -> Result<(), MiniMessageError> {
263 match tag {
264 // Colors
265 "black" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Black)))?,
266 "dark_blue" => {
267 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkBlue)))?
268 }
269 "dark_green" => {
270 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkGreen)))?
271 }
272 "dark_aqua" => {
273 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkAqua)))?
274 }
275 "dark_red" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkRed)))?,
276 "dark_purple" => {
277 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkPurple)))?
278 }
279 "gold" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Gold)))?,
280 "gray" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Gray)))?,
281 "dark_gray" => {
282 self.push_style(|s| s.color = Some(Color::Named(NamedColor::DarkGray)))?
283 }
284 "blue" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Blue)))?,
285 "green" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Green)))?,
286 "aqua" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Aqua)))?,
287 "red" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Red)))?,
288 "light_purple" => {
289 self.push_style(|s| s.color = Some(Color::Named(NamedColor::LightPurple)))?
290 }
291 "yellow" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::Yellow)))?,
292 "white" => self.push_style(|s| s.color = Some(Color::Named(NamedColor::White)))?,
293 "color" | "colour" | "c" if !args.is_empty() => {
294 if let Some(color) = args[0].parse::<Color>().ok() {
295 self.push_style(|s| s.color = Some(color))?
296 }
297 }
298
299 // Decorations
300 "bold" | "b" => self.push_style(|s| s.bold = Some(true))?,
301 "italic" | "i" | "em" => self.push_style(|s| s.italic = Some(true))?,
302 "underlined" | "u" => self.push_style(|s| s.underlined = Some(true))?,
303 "strikethrough" | "st" => self.push_style(|s| s.strikethrough = Some(true))?,
304 "obfuscated" | "obf" => self.push_style(|s| s.obfuscated = Some(true))?,
305
306 // Reset tag
307 "reset" => self.reset_style()?,
308
309 // Click events
310 "click" if args.len() >= 2 => {
311 let action = args[0].as_str();
312 let value = args[1].as_str();
313 match action {
314 "run_command" => self.push_style(|s| {
315 s.click_event = Some(ClickEvent::RunCommand {
316 command: value.to_string(),
317 })
318 })?,
319 "suggest_command" => self.push_style(|s| {
320 s.click_event = Some(ClickEvent::SuggestCommand {
321 command: value.to_string(),
322 })
323 })?,
324 "open_url" => self.push_style(|s| {
325 s.click_event = Some(ClickEvent::OpenUrl {
326 url: value.to_string(),
327 })
328 })?,
329 "copy_to_clipboard" => self.push_style(|s| {
330 s.click_event = Some(ClickEvent::CopyToClipboard {
331 value: value.to_string(),
332 })
333 })?,
334 _ => {}
335 }
336 }
337
338 // Hover events
339 "hover" if !args.is_empty() => {
340 let action = args[0].as_str();
341 if action == "show_text" && args.len() >= 2 {
342 let mut nested_parser = Parser::new(&args[1], self.config);
343 let nested = nested_parser.parse()?;
344 self.push_style(|s| {
345 s.hover_event = Some(HoverEvent::ShowText { value: nested })
346 })?;
347 }
348 }
349
350 // Newline
351 "newline" | "br" => {
352 self.component_parts.push(Component::text("\n"));
353 }
354
355 // Insertion
356 "insert" | "insertion" if !args.is_empty() => {
357 self.push_style(|s| s.insertion = Some(args[0].clone()))?
358 }
359
360 // Handle self-closing tags
361 _ if self_closing => {
362 // For self-closing tags, create an empty component with the style
363 let current_style = self.current_style();
364 let mut comp = Component::text("");
365 comp = comp.color(current_style.color.clone());
366 comp = comp.decorations(&self.collect_decorations());
367 self.component_parts.push(comp);
368 }
369
370 // Unknown tags are treated as text
371 _ => {
372 let mut tag_text = format!("<{tag}");
373 for arg in args {
374 tag_text.push(':');
375 tag_text.push_str(&arg);
376 }
377 if self_closing {
378 tag_text.push('/');
379 }
380 tag_text.push('>');
381 self.component_parts
382 .push(Component::text(tag_text).apply_fallback_style(self.current_style()));
383 }
384 }
385
386 Ok(())
387 }Sourcepub fn click_event(self, event: Option<ClickEvent>) -> Self
pub fn click_event(self, event: Option<ClickEvent>) -> Self
Sets click event
Sourcepub fn hover_event(self, event: Option<HoverEvent>) -> Self
pub fn hover_event(self, event: Option<HoverEvent>) -> Self
Sets hover event
Sourcepub fn has_decoration(&self, decoration: TextDecoration) -> bool
pub fn has_decoration(&self, decoration: TextDecoration) -> bool
Checks if a decoration is enabled
Sourcepub fn has_styling(&self) -> bool
pub fn has_styling(&self) -> bool
Checks if any styling is present
Sourcepub fn set_children(self, children: Vec<Component>) -> Self
pub fn set_children(self, children: Vec<Component>) -> Self
Sets child components
Sourcepub fn get_children(&self) -> &[Component]
pub fn get_children(&self) -> &[Component]
Gets child components