mc_legacy_formatting/lib.rs
1//! A parser for Minecraft's [legacy formatting system][legacy_fmt], created
2//! with careful attention to the quirks of the vanilla client's implementation.
3//!
4//! # Features
5//!
6//! * Iterator-based, non-allocating parser
7//! * Supports `#![no_std]` usage (with `default-features` set to `false`)
8//! * Implements the entire spec as well as vanilla client quirks (such as handling
9//! of whitespace with the `STRIKETHROUGH` style)
10//! * Helpers for pretty-printing the parsed [`Span`]s to the terminal
11//! * Support for parsing any start character for the formatting codes (vanilla
12//! uses `§` while many community tools use `&`)
13//!
14//! # Examples
15//!
16//! Using [`SpanIter`]:
17//!
18//! ```
19//! use mc_legacy_formatting::{SpanExt, Span, Color, Styles};
20//!
21//! let s = "§4This will be dark red §oand italic";
22//! let mut span_iter = s.span_iter();
23//!
24//! assert_eq!(span_iter.next().unwrap(), Span::new_styled("This will be dark red ", Color::DarkRed, Styles::empty()));
25//! assert_eq!(span_iter.next().unwrap(), Span::new_styled("and italic", Color::DarkRed, Styles::ITALIC));
26//! assert!(span_iter.next().is_none());
27//! ```
28//!
29//! With a custom start character:
30//!
31//! ```
32//! use mc_legacy_formatting::{SpanExt, Span, Color, Styles};
33//!
34//! let s = "&6It's a lot easier to type &b& &6than &b§";
35//! let mut span_iter = s.span_iter().with_start_char('&');
36//!
37//! assert_eq!(span_iter.next().unwrap(), Span::new_styled("It's a lot easier to type ", Color::Gold, Styles::empty()));
38//! assert_eq!(span_iter.next().unwrap(), Span::new_styled("& ", Color::Aqua, Styles::empty()));
39//! assert_eq!(span_iter.next().unwrap(), Span::new_styled("than ", Color::Gold, Styles::empty()));
40//! assert_eq!(span_iter.next().unwrap(), Span::new_styled("§", Color::Aqua, Styles::empty()));
41//! assert!(span_iter.next().is_none());
42//! ```
43//!
44//! [legacy_fmt]: https://wiki.vg/Chat#Colors
45
46#![no_std]
47#![deny(missing_docs)]
48#![deny(unused_must_use)]
49
50/// Bring `std` in for testing
51#[cfg(test)]
52extern crate std;
53
54use core::str::CharIndices;
55
56use bitflags::bitflags;
57
58#[cfg(feature = "color-print")]
59mod color_print;
60
61#[cfg(feature = "color-print")]
62pub use color_print::PrintSpanColored;
63
64/// An extension trait that adds a method for creating a [`SpanIter`]
65pub trait SpanExt {
66 /// Produces a [`SpanIter`] from `&self`
67 ///
68 /// # Examples
69 ///
70 /// ```
71 /// use mc_legacy_formatting::{SpanExt, Span, Color, Styles};
72 ///
73 /// let s = "§4This will be dark red §oand italic";
74 /// let mut span_iter = s.span_iter();
75 ///
76 /// assert_eq!(span_iter.next().unwrap(), Span::new_styled("This will be dark red ", Color::DarkRed, Styles::empty()));
77 /// assert_eq!(span_iter.next().unwrap(), Span::new_styled("and italic", Color::DarkRed, Styles::ITALIC));
78 /// assert!(span_iter.next().is_none());
79 /// ```
80 fn span_iter(&self) -> SpanIter;
81}
82
83impl<T: AsRef<str>> SpanExt for T {
84 fn span_iter(&self) -> SpanIter {
85 SpanIter::new(self.as_ref())
86 }
87}
88
89/// An iterator that yields [`Span`]s from an input string.
90///
91/// # Examples
92///
93/// ```
94/// use mc_legacy_formatting::{SpanIter, Span, Color, Styles};
95///
96/// let s = "§4This will be dark red §oand italic";
97/// let mut span_iter = SpanIter::new(s);
98///
99/// assert_eq!(span_iter.next().unwrap(), Span::new_styled("This will be dark red ", Color::DarkRed, Styles::empty()));
100/// assert_eq!(span_iter.next().unwrap(), Span::new_styled("and italic", Color::DarkRed, Styles::ITALIC));
101/// assert!(span_iter.next().is_none());
102/// ```
103#[derive(Debug, Clone)]
104pub struct SpanIter<'a> {
105 buf: &'a str,
106 chars: CharIndices<'a>,
107 /// The character that indicates the beginning of a fmt code
108 ///
109 /// The vanilla client uses `§` for this, but community tooling often uses
110 /// `&`, so we allow it to be configured
111 start_char: char,
112 color: Color,
113 styles: Styles,
114 finished: bool,
115}
116
117impl<'a> SpanIter<'a> {
118 /// Create a new [`SpanIter`] to parse the given string
119 pub fn new(s: &'a str) -> Self {
120 Self {
121 buf: s,
122 chars: s.char_indices(),
123 start_char: '§',
124 color: Color::White,
125 styles: Styles::default(),
126 finished: false,
127 }
128 }
129
130 /// Set the start character used while parsing
131 ///
132 /// # Examples
133 ///
134 /// ```
135 /// use mc_legacy_formatting::{SpanIter, Span, Color, Styles};
136 ///
137 /// let s = "&6It's a lot easier to type &b& &6than &b§";
138 /// let mut span_iter = SpanIter::new(s).with_start_char('&');
139 ///
140 /// assert_eq!(span_iter.next().unwrap(), Span::new_styled("It's a lot easier to type ", Color::Gold, Styles::empty()));
141 /// assert_eq!(span_iter.next().unwrap(), Span::new_styled("& ", Color::Aqua, Styles::empty()));
142 /// assert_eq!(span_iter.next().unwrap(), Span::new_styled("than ", Color::Gold, Styles::empty()));
143 /// assert_eq!(span_iter.next().unwrap(), Span::new_styled("§", Color::Aqua, Styles::empty()));
144 /// assert!(span_iter.next().is_none());
145 /// ```
146 pub fn with_start_char(mut self, c: char) -> Self {
147 self.start_char = c;
148 self
149 }
150
151 /// Set the start character used while parsing
152 pub fn set_start_char(&mut self, c: char) {
153 self.start_char = c;
154 }
155
156 /// Update the currently stored color
157 fn update_color(&mut self, color: Color) {
158 self.color = color;
159 // According to https://wiki.vg/Chat, using a color code resets the current
160 // style
161 self.styles = Styles::empty();
162 }
163
164 /// Insert `styles` into the currently stored styles
165 fn update_styles(&mut self, styles: Styles) {
166 self.styles.insert(styles);
167 }
168
169 /// Should be called when encountering the `RESET` fmt code
170 fn reset_styles(&mut self) {
171 self.color = Color::White;
172 self.styles = Styles::empty();
173 }
174
175 /// Make a [`Span`] based off the current state of the iterator
176 ///
177 /// The span will be from `start..end`
178 fn make_span(&self, start: usize, end: usize) -> Span<'a> {
179 if self.color == Color::White && self.styles.is_empty() {
180 Span::Plain(&self.buf[start..end])
181 } else {
182 let text = &self.buf[start..end];
183
184 // The vanilla client renders whitespace with `Styles::STRIKETHROUGH`
185 // as a solid line. This replicates that behavior
186 //
187 // (Technically it does this by drawing a line over any text slice
188 // with the `STRIKETHROUGH` style.)
189 if text.chars().all(|c| c.is_ascii_whitespace())
190 && self.styles.contains(Styles::STRIKETHROUGH)
191 {
192 Span::StrikethroughWhitespace {
193 text,
194 color: self.color,
195 styles: self.styles,
196 }
197 } else {
198 Span::Styled {
199 text,
200 color: self.color,
201 styles: self.styles,
202 }
203 }
204 }
205 }
206}
207
208/// Keeps track of the state for each iteration
209#[derive(Debug, Copy, Clone)]
210enum SpanIterState {
211 GatheringStyles(GatheringStylesState),
212 GatheringText(GatheringTextState),
213}
214
215/// In this state we are at the beginning of an iteration and we are looking to
216/// handle any initial formatting codes
217#[derive(Debug, Copy, Clone)]
218enum GatheringStylesState {
219 /// We're looking for our start char
220 ExpectingStartChar,
221 /// We've found our start char and are expecting a fmt code after it
222 ExpectingFmtCode,
223}
224
225/// In this state we've encountered text unrelated to formatting, which means
226/// the next valid fmt code we encounter ends this iteration
227#[derive(Debug, Copy, Clone)]
228enum GatheringTextState {
229 /// We're waiting to find our start char
230 WaitingForStartChar,
231 /// We've found our start char and are expecting a fmt code after it
232 ///
233 /// If we find a valid fmt code in this state, we need to make a span, apply
234 /// this last fmt code to our state, and end this iteration.
235 ExpectingEndChar,
236}
237
238impl<'a> Iterator for SpanIter<'a> {
239 type Item = Span<'a>;
240
241 fn next(&mut self) -> Option<Self::Item> {
242 use GatheringStylesState::*;
243 use GatheringTextState::*;
244 use SpanIterState::*;
245
246 if self.finished {
247 return None;
248 }
249 let mut state = GatheringStyles(ExpectingStartChar);
250 let mut span_start = None;
251 let mut span_end = None;
252
253 while let Some((idx, c)) = self.chars.next() {
254 state = match state {
255 GatheringStyles(style_state) => match style_state {
256 ExpectingStartChar => {
257 span_start = Some(idx);
258 match c {
259 c if c == self.start_char => GatheringStyles(ExpectingFmtCode),
260 _ => GatheringText(WaitingForStartChar),
261 }
262 }
263 ExpectingFmtCode => {
264 if let Some(color) = Color::from_char(c) {
265 self.update_color(color);
266 span_start = None;
267 GatheringStyles(ExpectingStartChar)
268 } else if let Some(style) = Styles::from_char(c) {
269 self.update_styles(style);
270 span_start = None;
271 GatheringStyles(ExpectingStartChar)
272 } else if c == 'r' || c == 'R' {
273 // Handle the `RESET` fmt code
274
275 self.reset_styles();
276 span_start = None;
277 GatheringStyles(ExpectingStartChar)
278 } else {
279 GatheringText(WaitingForStartChar)
280 }
281 }
282 },
283 GatheringText(text_state) => match text_state {
284 WaitingForStartChar => match c {
285 c if c == self.start_char => {
286 span_end = Some(idx);
287 GatheringText(ExpectingEndChar)
288 }
289 _ => state,
290 },
291 ExpectingEndChar => {
292 // Note that we only end this iteration if we find a valid fmt code
293 //
294 // If we do, we make sure to apply it to our state so that we can
295 // pick up where we left off when the next iteration begins
296
297 if let Some(color) = Color::from_char(c) {
298 let span = self.make_span(span_start.unwrap(), span_end.unwrap());
299 self.update_color(color);
300 return Some(span);
301 } else if let Some(style) = Styles::from_char(c) {
302 let span = self.make_span(span_start.unwrap(), span_end.unwrap());
303 self.update_styles(style);
304 return Some(span);
305 } else if c == 'r' || c == 'R' {
306 // Handle the `RESET` fmt code
307
308 let span = self.make_span(span_start.unwrap(), span_end.unwrap());
309 self.reset_styles();
310 return Some(span);
311 } else {
312 span_end = None;
313 GatheringText(WaitingForStartChar)
314 }
315 }
316 },
317 }
318 }
319
320 self.finished = true;
321 span_start.map(|start| self.make_span(start, self.buf.len()))
322 }
323}
324
325/// Text with an associated color and associated styles.
326///
327/// [`Span`] implements [`Display`](core::fmt::Display) and can be neatly printed.
328#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
329pub enum Span<'a> {
330 /// A styled slice of text
331 Styled {
332 /// The styled text slice
333 text: &'a str,
334 /// The color of the text
335 color: Color,
336 /// Styles that should be applied to the text
337 styles: Styles,
338 },
339 /// An unbroken sequence of whitespace that was given the
340 /// [`STRIKETHROUGH`](Styles::STRIKETHROUGH) style.
341 ///
342 /// The vanilla client renders whitespace with the `STRIKETHROUGH` style
343 /// as a solid line; this variant allows for replicating that behavior.
344 StrikethroughWhitespace {
345 /// The styled whitespace slice
346 text: &'a str,
347 /// The color of the whitespace (and therefore the line over it)
348 color: Color,
349 /// Styles applied to the whitespace (will contain at least
350 /// [`STRIKETHROUGH`](Styles::STRIKETHROUGH))
351 styles: Styles,
352 },
353 /// An unstyled slice of text
354 ///
355 /// This should be given a default style. The vanilla client
356 /// would use [`Color::White`] and [`Styles::empty()`].
357 Plain(&'a str),
358}
359
360impl<'a> core::fmt::Display for Span<'a> {
361 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
362 match self {
363 // TODO: handle random style
364 Span::Styled { text, .. } => f.write_str(text),
365 Span::StrikethroughWhitespace { text, .. } => {
366 (0..text.len()).try_for_each(|_| f.write_str("-"))
367 }
368 Span::Plain(text) => f.write_str(text),
369 }
370 }
371}
372
373impl<'a> Span<'a> {
374 /// Create a new [`Span::Plain`]
375 pub fn new_plain(s: &'a str) -> Self {
376 Span::Plain(s)
377 }
378
379 /// Create a new [`Span::StrikethroughWhitespace`]
380 pub fn new_strikethrough_whitespace(s: &'a str, color: Color, styles: Styles) -> Self {
381 Span::StrikethroughWhitespace {
382 text: s,
383 color,
384 styles,
385 }
386 }
387
388 /// Create a new [`Span::Styled`]
389 pub fn new_styled(s: &'a str, color: Color, styles: Styles) -> Self {
390 Span::Styled {
391 text: s,
392 color,
393 styles,
394 }
395 }
396
397 /// Wraps this [`Span`] in a type that enables colored printing
398 #[cfg(feature = "color-print")]
399 pub fn wrap_colored(self) -> PrintSpanColored<'a> {
400 PrintSpanColored::from(self)
401 }
402}
403
404/// Various colors that a [`Span`] can have.
405///
406/// See [the wiki.vg docs][colors] for specific information.
407///
408/// [colors]: https://wiki.vg/Chat#Colors
409#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
410#[allow(missing_docs)]
411pub enum Color {
412 Black,
413 DarkBlue,
414 DarkGreen,
415 DarkAqua,
416 DarkRed,
417 DarkPurple,
418 Gold,
419 Gray,
420 DarkGray,
421 Blue,
422 Green,
423 Aqua,
424 Red,
425 LightPurple,
426 Yellow,
427 White,
428}
429
430impl Default for Color {
431 fn default() -> Self {
432 Color::White
433 }
434}
435
436impl Color {
437 /// Map a `char` to a [`Color`].
438 ///
439 /// Returns [`None`] if `c` didn't map to a [`Color`].
440 pub fn from_char(c: char) -> Option<Self> {
441 Some(match c {
442 '0' => Color::Black,
443 '1' => Color::DarkBlue,
444 '2' => Color::DarkGreen,
445 '3' => Color::DarkAqua,
446 '4' => Color::DarkRed,
447 '5' => Color::DarkPurple,
448 '6' => Color::Gold,
449 '7' => Color::Gray,
450 '8' => Color::DarkGray,
451 '9' => Color::DarkBlue,
452 // The vanilla client accepts lower or uppercase interchangeably
453 'a' | 'A' => Color::Green,
454 'b' | 'B' => Color::Aqua,
455 'c' | 'C' => Color::Red,
456 'd' | 'D' => Color::LightPurple,
457 'e' | 'E' => Color::Yellow,
458 'f' | 'F' => Color::White,
459 _ => return None,
460 })
461 }
462
463 /// Get the correct foreground hex color string for a given color
464 ///
465 /// # Examples
466 ///
467 /// ```
468 /// use mc_legacy_formatting::Color;
469 /// assert_eq!(Color::Aqua.foreground_hex_str(), "#55ffff");
470 /// ```
471 pub const fn foreground_hex_str(&self) -> &'static str {
472 match self {
473 Color::Black => "#000000",
474 Color::DarkBlue => "#0000aa",
475 Color::DarkGreen => "#00aa00",
476 Color::DarkAqua => "#00aaaa",
477 Color::DarkRed => "#aa0000",
478 Color::DarkPurple => "#aa00aa",
479 Color::Gold => "#ffaa00",
480 Color::Gray => "#aaaaaa",
481 Color::DarkGray => "#555555",
482 Color::Blue => "#5555ff",
483 Color::Green => "#55ff55",
484 Color::Aqua => "#55ffff",
485 Color::Red => "#ff5555",
486 Color::LightPurple => "#ff55ff",
487 Color::Yellow => "#ffff55",
488 Color::White => "#ffffff",
489 }
490 }
491
492 /// Get the correct background hex color string for a given color
493 ///
494 /// # Examples
495 ///
496 /// ```
497 /// use mc_legacy_formatting::Color;
498 /// assert_eq!(Color::Aqua.background_hex_str(), "#153f3f");
499 /// ```
500 pub const fn background_hex_str(&self) -> &'static str {
501 match self {
502 Color::Black => "#000000",
503 Color::DarkBlue => "#00002a",
504 Color::DarkGreen => "#002a00",
505 Color::DarkAqua => "#002a2a",
506 Color::DarkRed => "#2a0000",
507 Color::DarkPurple => "#2a002a",
508 Color::Gold => "#2a2a00",
509 Color::Gray => "#2a2a2a",
510 Color::DarkGray => "#151515",
511 Color::Blue => "#15153f",
512 Color::Green => "#153f15",
513 Color::Aqua => "#153f3f",
514 Color::Red => "#3f1515",
515 Color::LightPurple => "#3f153f",
516 Color::Yellow => "#3f3f15",
517 Color::White => "#3f3f3f",
518 }
519 }
520
521 /// Get the correct foreground RGB color values for a given color
522 ///
523 /// Returns (red, green, blue)
524 ///
525 /// # Examples
526 ///
527 /// ```
528 /// use mc_legacy_formatting::Color;
529 /// assert_eq!(Color::Aqua.foreground_rgb(), (85, 255, 255));
530 /// ```
531 pub const fn foreground_rgb(&self) -> (u8, u8, u8) {
532 match self {
533 Color::Black => (0, 0, 0),
534 Color::DarkBlue => (0, 0, 170),
535 Color::DarkGreen => (0, 170, 0),
536 Color::DarkAqua => (0, 170, 170),
537 Color::DarkRed => (170, 0, 0),
538 Color::DarkPurple => (170, 0, 170),
539 Color::Gold => (255, 170, 0),
540 Color::Gray => (170, 170, 170),
541 Color::DarkGray => (85, 85, 85),
542 Color::Blue => (85, 85, 255),
543 Color::Green => (85, 255, 85),
544 Color::Aqua => (85, 255, 255),
545 Color::Red => (255, 85, 85),
546 Color::LightPurple => (255, 85, 255),
547 Color::Yellow => (255, 255, 85),
548 Color::White => (255, 255, 255),
549 }
550 }
551
552 /// Get the correct background RGB color values for a given color
553 ///
554 /// Returns (red, green, blue)
555 ///
556 /// # Examples
557 ///
558 /// ```
559 /// use mc_legacy_formatting::Color;
560 /// assert_eq!(Color::Aqua.background_rgb(), (21, 63, 63));
561 /// ```
562 pub const fn background_rgb(&self) -> (u8, u8, u8) {
563 match self {
564 Color::Black => (0, 0, 0),
565 Color::DarkBlue => (0, 0, 42),
566 Color::DarkGreen => (0, 42, 0),
567 Color::DarkAqua => (0, 42, 42),
568 Color::DarkRed => (42, 0, 0),
569 Color::DarkPurple => (42, 0, 42),
570 Color::Gold => (42, 42, 0),
571 Color::Gray => (42, 42, 42),
572 Color::DarkGray => (21, 21, 21),
573 Color::Blue => (21, 21, 63),
574 Color::Green => (21, 63, 21),
575 Color::Aqua => (21, 63, 63),
576 Color::Red => (63, 21, 21),
577 Color::LightPurple => (63, 21, 63),
578 Color::Yellow => (63, 63, 21),
579 Color::White => (63, 63, 63),
580 }
581 }
582}
583
584bitflags! {
585 /// Styles that can be combined and applied to a [`Span`].
586 ///
587 /// The `RESET` flag is missing because the parser implemented in [`SpanIter`]
588 /// takes care of it for you.
589 ///
590 /// See [wiki.vg's docs][styles] for detailed info about each style.
591 ///
592 /// # Examples
593 ///
594 /// ```
595 /// use mc_legacy_formatting::Styles;
596 /// let styles = Styles::BOLD | Styles::ITALIC | Styles::UNDERLINED;
597 ///
598 /// assert!(styles.contains(Styles::BOLD));
599 /// assert!(!styles.contains(Styles::RANDOM));
600 /// ```
601 ///
602 /// [styles]: https://wiki.vg/Chat#Styles
603 #[derive(Default)]
604 pub struct Styles: u32 {
605 /// Signals that the `Span`'s text should be replaced with randomized
606 /// characters at a constant interval
607 const RANDOM = 0b00000001;
608 /// Signals that the `Span`'s text should be bold
609 const BOLD = 0b00000010;
610 /// Signals that the `Span`'s text should be strikethrough
611 const STRIKETHROUGH = 0b00000100;
612 /// Signals that the `Span`'s text should be underlined
613 const UNDERLINED = 0b00001000;
614 /// Signals that the `Span`'s text should be italic
615 const ITALIC = 0b00010000;
616 }
617}
618
619impl Styles {
620 /// Map a `char` to a [`Styles`] object.
621 ///
622 /// Returns [`None`] if `c` didn't map to a [`Styles`] object.
623 pub fn from_char(c: char) -> Option<Self> {
624 Some(match c {
625 // The vanilla client accepts lower or uppercase interchangeably
626 'k' | 'K' => Styles::RANDOM,
627 'l' | 'L' => Styles::BOLD,
628 'm' | 'M' => Styles::STRIKETHROUGH,
629 'n' | 'N' => Styles::UNDERLINED,
630 'o' | 'O' => Styles::ITALIC,
631 _ => return None,
632 })
633 }
634}