1use crate::parsing::{ComponentParser, ComponentSerializer};
7use crate::{
8 ClickEvent, Color, Component, ComponentObject, HoverEvent, NamedColor, Style, TextDecoration,
9};
10use std::collections::HashMap;
11use std::error::Error;
12use std::fmt;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct MiniMessageError(String);
17
18impl fmt::Display for MiniMessageError {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 write!(f, "MiniMessage error: {}", self.0)
21 }
22}
23
24impl Error for MiniMessageError {}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq, Copy, Hash)]
28pub struct MiniMessageConfig {
29 pub strict: bool,
31 pub parse_legacy_colors: bool,
33}
34
35#[derive(Debug, Clone, Copy)]
37pub struct MiniMessage {
38 config: MiniMessageConfig,
39}
40
41impl MiniMessage {
42 pub fn new() -> Self {
44 Self::with_config(Default::default())
45 }
46
47 pub fn with_config(config: MiniMessageConfig) -> Self {
49 MiniMessage { config }
50 }
51
52 pub fn parse(&self, input: impl AsRef<str>) -> Result<Component, MiniMessageError> {
54 let mut parser = Parser::new(input.as_ref(), &self.config);
55 parser.parse()
56 }
57}
58
59impl Default for MiniMessage {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl ComponentParser for MiniMessage {
66 type Err = MiniMessageError;
67
68 fn from_string(input: impl AsRef<str>) -> Result<Component, Self::Err> {
70 let config = MiniMessageConfig::default();
73 let mut parser = Parser::new(input.as_ref(), &config);
74 parser.parse()
75 }
76}
77
78impl ComponentSerializer for MiniMessage {
79 type Err = MiniMessageError;
80
81 fn to_string(component: &Component) -> Result<String, Self::Err> {
82 Serializer::new().serialize(component)
83 }
84}
85
86struct Parser<'a> {
88 input: &'a str,
89 position: usize,
90 config: &'a MiniMessageConfig,
91 style_stack: Vec<Style>,
92 component_parts: Vec<Component>,
93}
94
95impl<'a> Parser<'a> {
96 fn new(input: &'a str, config: &'a MiniMessageConfig) -> Self {
97 Self {
98 input,
99 position: 0,
100 config,
101 style_stack: vec![Style::default()],
102 component_parts: Vec::new(),
103 }
104 }
105
106 fn parse(&mut self) -> Result<Component, MiniMessageError> {
107 while self.position < self.input.len() {
108 if self.starts_with('<') {
109 self.parse_tag()?;
110 } else {
111 self.parse_text()?;
112 }
113 }
114
115 let parts = std::mem::take(&mut self.component_parts);
116 if parts.len() == 1 {
117 Ok(parts.into_iter().next().unwrap())
119 } else {
120 Ok(Component::Array(parts))
121 }
122 }
123
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 self.position += 1;
147
148 if self.starts_with('/') {
149 self.position += 1;
151 let tag_name = self.read_tag_name()?;
152 self.handle_close_tag(&tag_name)?;
153 self.expect('>')?;
154 } else {
155 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 self.skip_whitespace();
163 while self.starts_with(':') {
165 self.position += 1;
166 self.skip_whitespace();
167 }
168
169 if self.starts_with('>') || self.starts_with('/') {
171 break;
172 }
173
174 let arg = self.read_argument()?;
176 args.push(arg);
177 }
178
179 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 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 "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 "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" => self.reset_style()?,
308
309 "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" 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" | "br" => {
352 self.component_parts.push(Component::text("\n"));
353 }
354
355 "insert" | "insertion" if !args.is_empty() => {
357 self.push_style(|s| s.insertion = Some(args[0].clone()))?
358 }
359
360 _ if self_closing => {
362 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 _ => {
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 }
388
389 fn handle_close_tag(&mut self, tag: &str) -> Result<(), MiniMessageError> {
390 match tag {
391 "bold" | "b" | "italic" | "i" | "em" | "underlined" | "u" | "strikethrough" | "st"
392 | "obfuscated" | "obf" | "color" | "colour" | "c" | "click" | "hover" | "insert"
393 | "insertion" => {
394 self.pop_style()?;
395 }
396 _ => {
397 if self.style_stack.len() > 1 {
399 self.style_stack.pop();
400 }
401 }
402 }
403 Ok(())
404 }
405
406 fn push_style<F>(&mut self, modifier: F) -> Result<(), MiniMessageError>
407 where
408 F: FnOnce(&mut Style),
409 {
410 let mut new_style = self.current_style().clone();
411 modifier(&mut new_style);
412 self.style_stack.push(new_style);
413 Ok(())
414 }
415
416 fn pop_style(&mut self) -> Result<(), MiniMessageError> {
417 if self.style_stack.len() > 1 {
418 self.style_stack.pop();
419 Ok(())
420 } else {
421 Err(MiniMessageError("Unbalanced closing tag".to_string()))
422 }
423 }
424
425 fn reset_style(&mut self) -> Result<(), MiniMessageError> {
426 while self.style_stack.len() > 1 {
427 self.style_stack.pop();
428 }
429 Ok(())
430 }
431
432 fn current_style(&self) -> &Style {
433 self.style_stack.last().unwrap()
435 }
436
437 fn collect_decorations(&self) -> HashMap<TextDecoration, Option<bool>> {
438 let style = self.current_style();
439 let mut decorations = HashMap::new();
440 if let Some(bold) = style.bold {
441 decorations.insert(TextDecoration::Bold, Some(bold));
442 }
443 if let Some(italic) = style.italic {
444 decorations.insert(TextDecoration::Italic, Some(italic));
445 }
446 if let Some(underlined) = style.underlined {
447 decorations.insert(TextDecoration::Underlined, Some(underlined));
448 }
449 if let Some(strikethrough) = style.strikethrough {
450 decorations.insert(TextDecoration::Strikethrough, Some(strikethrough));
451 }
452 if let Some(obfuscated) = style.obfuscated {
453 decorations.insert(TextDecoration::Obfuscated, Some(obfuscated));
454 }
455 decorations
456 }
457
458 fn starts_with(&self, c: char) -> bool {
459 self.input[self.position..].starts_with(c)
460 }
461
462 fn current_char(&self) -> char {
463 self.input[self.position..].chars().next().unwrap_or('\0')
464 }
465
466 fn skip_whitespace(&mut self) {
467 while self.position < self.input.len() {
468 if !self.input[self.position..].starts_with(char::is_whitespace) {
469 break;
470 }
471 self.position += 1;
472 }
473 }
474
475 fn expect(&mut self, c: char) -> Result<(), MiniMessageError> {
476 if self.position < self.input.len() && self.current_char() == c {
477 self.position += 1;
478 Ok(())
479 } else {
480 Err(MiniMessageError(format!("Expected '{c}'")))
481 }
482 }
483}
484
485struct Serializer {
487 output: String,
488 current_style: Style,
489}
490
491impl Serializer {
492 fn new() -> Self {
493 Self {
494 output: String::new(),
495 current_style: Style::default(),
496 }
497 }
498
499 fn serialize(&mut self, component: &Component) -> Result<String, MiniMessageError> {
500 self.serialize_component(component)?;
501 Ok(self.output.clone())
502 }
503
504 fn serialize_component(&mut self, component: &Component) -> Result<(), MiniMessageError> {
505 match component {
506 Component::String(s) => self.serialize_text(s),
507 Component::Array(components) => {
508 let base_style = self.current_style.clone();
509 for comp in components {
510 self.current_style = base_style.clone();
512 self.serialize_component(comp)?;
513 }
514 Ok(())
515 }
516 Component::Object(obj) => self.serialize_object(obj),
517 }
518 }
519
520 fn serialize_object(&mut self, obj: &ComponentObject) -> Result<(), MiniMessageError> {
521 let prev_style = self.current_style.clone();
523
524 let mut style_changes = Vec::new();
526
527 if let Some(color) = &obj.color
528 && Some(color) != prev_style.color.as_ref()
529 {
530 if let Some(named) = color.to_named() {
531 style_changes.push(named.to_string());
532 } else if let Color::Hex(hex) = color {
533 style_changes.push(format!("color:{hex}"));
534 }
535 }
536
537 if obj.bold != prev_style.bold && obj.bold == Some(true) {
538 style_changes.push("bold".to_string());
539 }
540
541 if obj.italic != prev_style.italic && obj.italic == Some(true) {
542 style_changes.push("italic".to_string());
543 }
544
545 if obj.underlined != prev_style.underlined && obj.underlined == Some(true) {
546 style_changes.push("underlined".to_string());
547 }
548
549 if obj.strikethrough != prev_style.strikethrough && obj.strikethrough == Some(true) {
550 style_changes.push("strikethrough".to_string());
551 }
552
553 if obj.obfuscated != prev_style.obfuscated && obj.obfuscated == Some(true) {
554 style_changes.push("obfuscated".to_string());
555 }
556
557 for change in &style_changes {
559 self.output.push_str(&format!("<{change}>"));
560 }
561
562 self.current_style = Style {
564 color: obj.color.clone(),
565 bold: obj.bold,
566 italic: obj.italic,
567 underlined: obj.underlined,
568 strikethrough: obj.strikethrough,
569 obfuscated: obj.obfuscated,
570 ..self.current_style.clone()
571 };
572
573 if let Some(text) = &obj.text {
575 self.serialize_text(text)?;
576 }
577
578 if let Some(extra) = &obj.extra {
580 for comp in extra {
581 self.serialize_component(comp)?;
582 }
583 }
584
585 for change in style_changes.iter().rev() {
587 self.output.push_str(&format!("</{change}>"));
588 }
589
590 self.current_style = prev_style;
592
593 Ok(())
594 }
595
596 fn serialize_text(&mut self, text: &str) -> Result<(), MiniMessageError> {
597 for c in text.chars() {
599 match c {
600 '<' => self.output.push_str("<"),
601 '>' => self.output.push_str(">"),
602 '&' => self.output.push_str("&"),
603 _ => self.output.push(c),
604 }
605 }
606 Ok(())
607 }
608}
609
610
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use crate::{Component, NamedColor};
616
617 #[test]
618 fn test_parse_simple() {
619 let mm = MiniMessage::new();
620 let comp = mm.parse("Hello <red>world</red>!").unwrap();
621
622 if let Component::Array(parts) = comp {
623 assert_eq!(parts.len(), 3);
624 assert_eq!(parts[0].get_plain_text().unwrap(), "Hello ");
625 assert_eq!(parts[1].get_plain_text().unwrap(), "world");
626 assert_eq!(parts[2].get_plain_text().unwrap(), "!");
627 } else {
628 panic!("Expected array component");
629 }
630 }
631
632 #[test]
633 fn test_parse_nested() {
634 let mm = MiniMessage::new();
635 let comp = mm
636 .parse("Click <hover:show_text:'<red>Action!'>here</hover>")
637 .expect("Failed to parse component");
638
639 if let Component::Object(obj) = &comp
641 && let Some(children) = &obj.extra
642 {
643 if let Component::Object(hover_obj) = &children[1]
644 && let Some(hover_event) = &hover_obj.hover_event
645 {
646 match hover_event {
647 HoverEvent::ShowText { value } => {
648 assert_eq!(value.get_plain_text().unwrap(), "Action!");
649 }
650 _ => panic!("Expected show_text hover event"),
651 }
652 }
653 }
654 }
655
656 #[test]
657 fn test_serialize_simple() {
658 let comp = Component::text("Hello ")
659 .color(Some(Color::Named(NamedColor::Yellow)))
660 .append(Component::text("world").color(Some(Color::Named(NamedColor::Red))));
661
662 let result = MiniMessage::to_string(&comp).unwrap();
663 assert_eq!(result, "<yellow>Hello <red>world</red></yellow>");
665 }
666}