text_style/lib.rs
1// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
2// SPDX-License-Identifier: Apache-2.0 or MIT
3
4//! The `text_style` crate provides types and conversions for styled text.
5//!
6//! # Overview
7//!
8//! The central types of this crate are [`StyledStr`][] and [`StyledString`][]: owned and borrowed
9//! strings that are annotated with an optional style information, [`Style`][]. This style
10//! information consists of foreground and background colors ([`Color`][]) and multiple effects
11//! ([`Effect`][]: bold, italic, underline or strikeout).
12//!
13//! `text_style`’s types can be created directly or converted from or to several formats (all
14//! optional and activated by features):
15//!
16//! - [`ansi_term`][]: convert to [`ansi_term::ANSIString`][]
17//! - [`crossterm`][]: convert to [`crossterm::style::StyledContent`][]
18//! - [`cursive`][]: convert to [`cursive::utils::markup::StyledString`][]
19//! - [`genpdf`][]: convert to [`genpdf::style::StyledStr`][] and [`genpdf::style::StyledString`][]
20//! - [`syntect`][]: convert from [`syntect::highlighting::Style`][]
21//! - [`termion`][]: convert to a termion escape string
22//!
23//! # Background
24//!
25//! There is a plethora of crates that produce or consume styled text. Most of these crates use
26//! very similar types: ANSI and RGB colors, text effects, styled strings. But converting between
27//! these types requires a lot of boilerplate code. The goal of this crate is to provide a subset
28//! of common style types to enable conversion between the different crates.
29//!
30//! # Usage
31//!
32//! ## Creating styled text
33//!
34//! The [`StyledString`][] and [`StyledStr`][] structs provide many utility methods for creating
35//! styled strings easily:
36//!
37//! ```
38//! use text_style::{AnsiColor, Effect, StyledStr};
39//! let s = StyledStr::plain("text")
40//! .with(AnsiColor::Red.light())
41//! .on(AnsiColor::Green.dark())
42//! .bold();
43//! text_style::ansi_term::render(std::io::stdout(), s)
44//! .expect("Could not render line");
45//! ```
46//!
47//! If the `syntect` feature is activated, conversion traits from `syntect`’s style types are
48//! implemented:
49//!
50//! ```
51//! use syntect::{easy, parsing, highlighting, util};
52//!
53//! let ps = parsing::SyntaxSet::load_defaults_newlines();
54//! let ts = highlighting::ThemeSet::load_defaults();
55//!
56//! let syntax = ps.find_syntax_by_extension("rs").unwrap();
57//! let mut h = easy::HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
58//! let s = "pub struct Wow { hi: u64 }\nfn blah() -> u64 {}";
59//! for line in util::LinesWithEndings::from(s) {
60//! let ranges: Vec<(highlighting::Style, &str)> = h.highlight(line, &ps);
61//! text_style::ansi_term::render_iter(std::io::stdout(), ranges.iter())
62//! .expect("Could not render line");
63//! }
64//! ```
65//!
66//! ## Rendering styled text
67//!
68//! The backends define conversion traits from or to the `text_style` types where applicable.
69//!
70//! Most backends also define `render` and `render_iter` methods to display a styled string or an
71//! iterator over styled strings:
72//!
73//! ```
74//! let s = text_style::StyledStr::plain("test").bold();
75//!
76//! let mut w = std::io::stdout();
77//! text_style::ansi_term::render(&mut w, &s).expect("Rendering failed");
78//! text_style::crossterm::render(&mut w, &s).expect("Rendering failed");
79//! text_style::termion::render(&mut w, &s).expect("Rendering failed");
80//! ```
81//!
82//! For more information, see the module documentations.
83//!
84//! [`Color`]: enum.Color.html
85//! [`Effect`]: enum.Effect.html
86//! [`Style`]: struct.Style.html
87//! [`StyledStr`]: struct.StyledStr.html
88//! [`StyledString`]: struct.StyledString.html
89//! [`ansi_term`]: ./ansi_term/index.html
90//! [`crossterm`]: ./crossterm/index.html
91//! [`cursive`]: ./cursive/index.html
92//! [`genpdf`]: ./genpdf/index.html
93//! [`syntect`]: ./syntect/index.html
94//! [`termion`]: ./termion/index.html
95//! [`ansi_term::ANSIString`]: https://docs.rs/ansi_term/latest/ansi_term/type.ANSIString.html
96//! [`crossterm::style::StyledContent`]: https://docs.rs/crossterm/latest/crossterm/style/struct.StyledContent.html
97//! [`cursive::utils::markup::StyledString`]: https://docs.rs/cursive/latest/cursive/utils/markup/type.StyledString.html
98//! [`genpdf::style::StyledStr`]: https://docs.rs/genpdf/latest/genpdf/style/struct.StyledStr.html
99//! [`genpdf::style::StyledString`]: https://docs.rs/genpdf/latest/genpdf/style/struct.StyledString.html
100//! [`syntect::highlighting::Style`]: https://docs.rs/syntect/latest/syntect/highlighting/struct.Style.html
101
102#![warn(missing_docs, rust_2018_idioms)]
103
104#[cfg(feature = "ansi_term")]
105pub mod ansi_term;
106#[cfg(feature = "crossterm")]
107pub mod crossterm;
108#[cfg(feature = "cursive")]
109pub mod cursive;
110#[cfg(feature = "genpdf")]
111pub mod genpdf;
112#[cfg(feature = "syntect")]
113pub mod syntect;
114#[cfg(feature = "termion")]
115pub mod termion;
116
117/// A borrowed string with an optional style annotation.
118///
119/// # Example
120///
121/// ```
122/// let s = text_style::StyledStr::plain("test").bold();
123///
124/// let s1 = text_style::StyledStr::styled("test", text_style::Style::fg(text_style::AnsiColor::Red.dark()));
125/// let s2 = text_style::StyledStr::plain("test").with(text_style::AnsiColor::Red.dark());
126/// assert_eq!(s1, s2);
127/// ```
128#[derive(Clone, Debug, PartialEq)]
129pub struct StyledStr<'a> {
130 /// The content of this string.
131 pub s: &'a str,
132 /// The style of this string.
133 pub style: Option<Style>,
134}
135
136/// An owned string with an optional style annotation.
137///
138/// # Example
139///
140/// ```
141/// let s = format!("Some number: {}", 42);
142///
143/// let s0 = text_style::StyledString::plain(s.clone()).bold();
144///
145/// let s1 = text_style::StyledString::styled(s.clone(), text_style::Style::fg(text_style::AnsiColor::Red.dark()));
146/// let s2 = text_style::StyledString::plain(s.clone()).with(text_style::AnsiColor::Red.dark());
147/// assert_eq!(s1, s2);
148/// ```
149#[derive(Clone, Debug, Default, PartialEq)]
150pub struct StyledString {
151 /// The content of this string.
152 pub s: String,
153 /// The style of this string.
154 pub style: Option<Style>,
155}
156
157/// A text style, a combination of a foreground color, a background color and text effects (all
158/// optional).
159#[derive(Clone, Copy, Debug, Default, PartialEq)]
160pub struct Style {
161 /// The foreground color (if set).
162 pub fg: Option<Color>,
163 /// The background color (if set).
164 pub bg: Option<Color>,
165 /// The text effects.
166 pub effects: Effects,
167}
168
169/// A text effect.
170#[derive(Clone, Copy, Debug, PartialEq)]
171pub enum Effect {
172 /// Bold text.
173 Bold,
174 /// Italic text.
175 Italic,
176 /// Underlined text.
177 Underline,
178 /// Struckthrough text.
179 Strikethrough,
180}
181
182/// All available text effects.
183pub const EFFECTS: &[Effect] = &[
184 Effect::Bold,
185 Effect::Italic,
186 Effect::Underline,
187 Effect::Strikethrough,
188];
189
190/// A set of text effects.
191#[derive(Clone, Copy, Debug, Default, PartialEq)]
192pub struct Effects {
193 /// Whether the bold text effect is set.
194 pub is_bold: bool,
195 /// Whether the italic text effect is set.
196 pub is_italic: bool,
197 /// Whether the underline text effect is set.
198 pub is_underline: bool,
199 /// Whether the strikethrough text effect is set.
200 pub is_strikethrough: bool,
201}
202
203/// An iterator over text effects.
204#[derive(Clone, Copy, Debug, PartialEq)]
205pub struct EffectsIter {
206 effects: Effects,
207 i: usize,
208}
209
210/// A color.
211///
212/// This enum stores colors, either as an ANSI color (see [`AnsiColor`][] and [`AnsiMode`][]) or as
213/// an RGB color.
214///
215/// [`AnsiColor`]: enum.AnsiColor.html
216/// [`AnsiMode`]: enum.AnsiMode.html
217#[derive(Clone, Copy, Debug, PartialEq)]
218pub enum Color {
219 /// An ANSI color.
220 Ansi {
221 /// The ANSI base color.
222 color: AnsiColor,
223 /// The variant of the ANSI base color (light or dark).
224 mode: AnsiMode,
225 },
226 /// An RGB color.
227 Rgb {
228 /// The red component.
229 r: u8,
230 /// The green component.
231 g: u8,
232 /// THe blue component.
233 b: u8,
234 },
235}
236
237/// An ANSI base color.
238///
239/// This enum contains the basic eight ANSI colors. These colors are available in two modes:
240/// [`Dark`][] and [`Light`][]. Combinations of an ANSI color and a mode are stored in the
241/// [`Color`][] enum.
242///
243/// [`Color`]: enum.Color.html
244/// [`Dark`]: enum.AnsiMode.html#variant.Dark
245/// [`Light`]: enum.AnsiMode.html#variant.Light
246#[derive(Clone, Copy, Debug, PartialEq)]
247pub enum AnsiColor {
248 /// Black (ANSI color #0 (dark) or #8 (light)).
249 Black,
250 /// Red (ANSI color #1 (dark) or #9 (light)).
251 Red,
252 /// Green (ANSI color #2 (dark) or #10 (light)).
253 Green,
254 /// Yellow (ANSI color #3 (dark) or #11 (light)).
255 Yellow,
256 /// Blue (ANSI color #4 (dark) or #12 (light)).
257 Blue,
258 /// Magenta (ANSI color #5 (dark) or #13 (light)).
259 Magenta,
260 /// Cyan (ANSI color #6 (dark) or #14 (light)).
261 Cyan,
262 /// White (ANSI color #7 (dark) or #15 (light)).
263 White,
264}
265
266/// An ANSI color mode.
267///
268/// The ANSI base colors, stored in the [`AnsiColor`][] enum, are available in two modes:
269/// [`Dark`][] and [`Light`][]. Combinations of an ANIS color and a mode are stored in the
270/// [`Color`][] enum.
271///
272/// [`AnsiColor`]: enum.AnsiColor.html
273/// [`Color`]: enum.Color.html
274/// [`Dark`]: #variant.Dark
275/// [`Light`]: #variant.Light
276#[derive(Clone, Copy, Debug, PartialEq)]
277pub enum AnsiMode {
278 /// The dark variant of an ANSI color.
279 Dark,
280 /// The light variant of an ANSI color.
281 Light,
282}
283
284impl<'a> StyledStr<'a> {
285 /// Creates a new styled string from the given string and an optional style.
286 pub fn new(s: &'a str, style: Option<Style>) -> StyledStr<'a> {
287 StyledStr { s, style }
288 }
289
290 /// Creates a new styled string from the given string and style.
291 pub fn styled(s: &'a str, style: Style) -> StyledStr<'a> {
292 StyledStr::new(s, Some(style))
293 }
294
295 /// Creates a new styled string from the given string without a style.
296 pub fn plain(s: &'a str) -> StyledStr<'a> {
297 StyledStr::new(s, None)
298 }
299
300 /// Sets the foreground color for this styled string.
301 pub fn with(mut self, fg: Color) -> Self {
302 self.style_mut().fg = Some(fg);
303 self
304 }
305
306 /// Sets the background color for this styled string.
307 pub fn on(mut self, bg: Color) -> Self {
308 self.style_mut().bg = Some(bg);
309 self
310 }
311
312 /// Sets the bold effect for this styled string.
313 pub fn bold(self) -> Self {
314 self.effect(Effect::Bold)
315 }
316
317 /// Sets the italic effect for this styled string.
318 pub fn italic(self) -> Self {
319 self.effect(Effect::Italic)
320 }
321
322 /// Sets the underline effect for this styled string.
323 pub fn underline(self) -> Self {
324 self.effect(Effect::Underline)
325 }
326
327 /// Sets the strikethrough effect for this styled string.
328 pub fn strikethrough(self) -> Self {
329 self.effect(Effect::Strikethrough)
330 }
331
332 /// Sets the given effect for this styled string.
333 pub fn effect(mut self, effect: Effect) -> Self {
334 self.style_mut().effects.set(effect, true);
335 self
336 }
337
338 /// Returns a mutable reference to the style of this string, creating a new style with the
339 /// default settings if the style is currently `None`.
340 pub fn style_mut(&mut self) -> &mut Style {
341 self.style.get_or_insert_with(Default::default)
342 }
343}
344
345impl StyledString {
346 /// Creates a new styled string from the given string and an optional style.
347 pub fn new(s: String, style: Option<Style>) -> StyledString {
348 StyledString { s, style }
349 }
350
351 /// Creates a new styled string from the given string and style.
352 pub fn styled(s: String, style: Style) -> StyledString {
353 StyledString::new(s, Some(style))
354 }
355
356 /// Creates a new styled string from the given string and style.
357 pub fn plain(s: String) -> StyledString {
358 StyledString::new(s, None)
359 }
360
361 /// Sets the foreground color for this styled string.
362 pub fn with(mut self, fg: Color) -> Self {
363 self.style_mut().fg = Some(fg);
364 self
365 }
366
367 /// Sets the background color for this styled string.
368 pub fn on(mut self, bg: Color) -> Self {
369 self.style_mut().bg = Some(bg);
370 self
371 }
372
373 /// Sets the bold effect for this styled string.
374 pub fn bold(self) -> Self {
375 self.effect(Effect::Bold)
376 }
377
378 /// Sets the italic effect for this styled string.
379 pub fn italic(self) -> Self {
380 self.effect(Effect::Italic)
381 }
382
383 /// Sets the underline effect for this styled string.
384 pub fn underline(self) -> Self {
385 self.effect(Effect::Underline)
386 }
387
388 /// Sets the strikethrough effect for this styled string.
389 pub fn strikethrough(self) -> Self {
390 self.effect(Effect::Strikethrough)
391 }
392
393 /// Sets the given effect for this styled string.
394 pub fn effect(mut self, effect: Effect) -> Self {
395 self.style_mut().effects.set(effect, true);
396 self
397 }
398
399 /// Returns a mutable reference to the style of this string, creating a new style with the
400 /// default settings if the style is currently `None`.
401 pub fn style_mut(&mut self) -> &mut Style {
402 self.style.get_or_insert_with(Default::default)
403 }
404}
405
406impl<'a, 'b> From<&'b StyledStr<'a>> for StyledStr<'a> {
407 fn from(s: &'b StyledStr<'a>) -> StyledStr<'a> {
408 StyledStr {
409 s: &s.s,
410 style: s.style,
411 }
412 }
413}
414
415impl<'a> From<&'a StyledString> for StyledStr<'a> {
416 fn from(s: &'a StyledString) -> StyledStr<'a> {
417 StyledStr {
418 s: &s.s,
419 style: s.style,
420 }
421 }
422}
423
424impl<'a> From<StyledStr<'a>> for StyledString {
425 fn from(s: StyledStr<'a>) -> StyledString {
426 StyledString {
427 s: s.s.to_owned(),
428 style: s.style,
429 }
430 }
431}
432
433impl<'a> From<&'a str> for StyledStr<'a> {
434 fn from(s: &'a str) -> StyledStr<'a> {
435 StyledStr::plain(s)
436 }
437}
438
439impl From<String> for StyledString {
440 fn from(s: String) -> StyledString {
441 StyledString::plain(s)
442 }
443}
444
445impl Style {
446 /// Creates a new style with the given foreground and background colors and effects.
447 pub fn new(fg: Option<Color>, bg: Option<Color>, effects: Effects) -> Style {
448 Style { fg, bg, effects }
449 }
450
451 /// Creates a new style with the given foreground color.
452 pub fn fg(color: Color) -> Style {
453 Style::new(Some(color), None, Effects::new())
454 }
455
456 /// Creates a new style with the given background color.
457 pub fn bg(color: Color) -> Style {
458 Style::new(None, Some(color), Effects::new())
459 }
460
461 /// Creates a new style with the given text effect.
462 pub fn effect(effect: Effect) -> Style {
463 Style::new(None, None, Effects::only(effect))
464 }
465
466 /// Creates a new style with the given text effects.
467 pub fn effects(effects: Effects) -> Style {
468 Style::new(None, None, effects)
469 }
470
471 /// Combines this style with another style.
472 ///
473 /// If a color is set by both styles, the current color is overwritten.
474 ///
475 /// # Example
476 ///
477 /// ```
478 /// use text_style::{AnsiColor, Effects, Style};
479 ///
480 /// assert_eq!(
481 /// Style::fg(AnsiColor::Red.dark()).and(Style::bg(AnsiColor::White.dark())),
482 /// Style::new(Some(AnsiColor::Red.dark()), Some(AnsiColor::White.dark()), Effects::empty()),
483 /// );
484 /// ```
485 pub fn and(mut self, style: Style) -> Style {
486 if let Some(fg) = style.fg {
487 self.fg = Some(fg);
488 }
489 if let Some(bg) = style.bg {
490 self.bg = Some(bg);
491 }
492 self.effects = self.effects.and(style.effects);
493 self
494 }
495
496 /// Sets the foreground color of this style.
497 pub fn set_fg(&mut self, color: Color) {
498 self.fg = Some(color);
499 }
500
501 /// Sets the background color of this style.
502 pub fn set_bg(&mut self, color: Color) {
503 self.bg = Some(color);
504 }
505
506 /// Sets or unsets the bold effect for this style.
507 pub fn set_bold(&mut self, bold: bool) {
508 self.effects.is_bold = bold;
509 }
510
511 /// Sets or unsets the italic effect for this style.
512 pub fn set_italic(&mut self, italic: bool) {
513 self.effects.is_italic = italic;
514 }
515
516 /// Sets or unsets the underline effect for this style.
517 pub fn set_underline(&mut self, underline: bool) {
518 self.effects.is_underline = underline;
519 }
520
521 /// Sets or unsets the strikethrough effect for this style.
522 pub fn strikethrough(&mut self, strikethrough: bool) {
523 self.effects.is_strikethrough = strikethrough;
524 }
525
526 /// Sets or unsets the given effect for this style.
527 pub fn set_effect(&mut self, effect: Effect, set: bool) {
528 self.effects.set(effect, set);
529 }
530}
531
532impl From<Effect> for Style {
533 fn from(effect: Effect) -> Style {
534 Style::effect(effect)
535 }
536}
537
538impl From<Effects> for Style {
539 fn from(effects: Effects) -> Style {
540 Style::effects(effects)
541 }
542}
543
544impl Effects {
545 /// Creates an empty set of text effects.
546 pub fn new() -> Effects {
547 Default::default()
548 }
549
550 /// Creates an empty set of text effects.
551 pub fn empty() -> Effects {
552 Effects::new()
553 }
554
555 /// Creates a set of text effects with only the given effect.
556 pub fn only(effect: Effect) -> Effects {
557 Effects::from(effect)
558 }
559
560 /// Sets or unsets the given text effect.
561 pub fn set(&mut self, effect: Effect, set: bool) {
562 match effect {
563 Effect::Bold => self.is_bold = set,
564 Effect::Italic => self.is_italic = set,
565 Effect::Underline => self.is_underline = set,
566 Effect::Strikethrough => self.is_strikethrough = set,
567 }
568 }
569
570 /// Checks whether the given effect is set.
571 pub fn is_set(&self, effect: Effect) -> bool {
572 match effect {
573 Effect::Bold => self.is_bold,
574 Effect::Italic => self.is_italic,
575 Effect::Underline => self.is_underline,
576 Effect::Strikethrough => self.is_strikethrough,
577 }
578 }
579
580 /// Combines this set of text effects with another set of text effects.
581 pub fn and(&self, other: Effects) -> Effects {
582 Effects {
583 is_bold: self.is_bold || other.is_bold,
584 is_italic: self.is_italic || other.is_italic,
585 is_underline: self.is_underline || other.is_underline,
586 is_strikethrough: self.is_strikethrough || other.is_strikethrough,
587 }
588 }
589
590 /// Checks whether this set of text effects is empty.
591 pub fn is_empty(&self) -> bool {
592 !self.is_bold && !self.is_italic && !self.is_underline && !self.is_strikethrough
593 }
594}
595
596impl std::iter::FromIterator<Effect> for Effects {
597 fn from_iter<I: IntoIterator<Item = Effect>>(iter: I) -> Effects {
598 let mut effects = Effects::new();
599 for effect in iter {
600 effects.set(effect, true);
601 }
602 effects
603 }
604}
605
606impl IntoIterator for Effects {
607 type Item = Effect;
608 type IntoIter = EffectsIter;
609
610 fn into_iter(self) -> EffectsIter {
611 EffectsIter::from(self)
612 }
613}
614
615impl From<Effect> for Effects {
616 fn from(effect: Effect) -> Effects {
617 let mut effects = Effects::new();
618 effects.set(effect, true);
619 effects
620 }
621}
622
623impl Iterator for EffectsIter {
624 type Item = Effect;
625
626 fn next(&mut self) -> Option<Effect> {
627 let mut next = None;
628 while let Some(effect) = EFFECTS.get(self.i) {
629 self.i += 1;
630 if self.effects.is_set(*effect) {
631 next = Some(*effect);
632 break;
633 }
634 }
635 next
636 }
637}
638
639impl From<Effects> for EffectsIter {
640 fn from(effects: Effects) -> EffectsIter {
641 EffectsIter { effects, i: 0 }
642 }
643}
644
645impl AnsiColor {
646 /// Returns the dark variant of this ANSI color.
647 pub fn dark(self) -> Color {
648 Color::Ansi {
649 color: self,
650 mode: AnsiMode::Dark,
651 }
652 }
653
654 /// Returns the light variant of this ANSI color.
655 pub fn light(self) -> Color {
656 Color::Ansi {
657 color: self,
658 mode: AnsiMode::Light,
659 }
660 }
661}