hi_doc/
formatting.rs

1use core::fmt;
2
3use annotated_string::{ApplyAnnotation, AnnotatedRope, Annotation};
4
5pub type Text = AnnotatedRope<Formatting>;
6
7#[derive(Default, Clone, PartialEq, Debug)]
8pub struct Formatting {
9	pub color: Option<u32>,
10	pub bg_color: Option<u32>,
11	pub bold: bool,
12	pub underline: bool,
13	pub decoration: bool,
14	pub url: Option<String>,
15}
16impl Annotation for Formatting {
17	fn try_merge(&mut self, other: &Self) -> bool {
18		self == other
19	}
20}
21
22impl ApplyAnnotation<Formatting> for Formatting {
23	fn apply(&mut self, change: &Formatting) {
24		if let Some(color) = change.color {
25			self.color = Some(color);
26		}
27		if let Some(bg_color) = change.bg_color {
28			self.bg_color = Some(bg_color);
29		}
30		if change.bold {
31			self.bold = true;
32		}
33		if change.underline {
34			self.underline = true;
35		}
36		if change.url.is_some() {
37			self.url = change.url.clone();
38		}
39	}
40}
41
42pub struct AddColorToUncolored(pub u32);
43impl ApplyAnnotation<AddColorToUncolored> for Formatting {
44	fn apply(&mut self, change: &AddColorToUncolored) {
45		if self.color.is_some() {
46			return;
47		}
48		self.color = Some(change.0);
49	}
50}
51
52impl Formatting {
53	pub fn listchar() -> Self {
54		Self {
55			color: Some(0x92837400),
56			decoration: true,
57			..Default::default()
58		}
59	}
60	pub fn line_number() -> Self {
61		Self {
62			color: Some(0x92837400),
63			// bg_color: Some(0x28282800),
64			..Default::default()
65		}
66	}
67	pub fn border() -> Self {
68		Self {
69			color: Some(0x92929200),
70			..Default::default()
71		}
72	}
73	pub fn filename() -> Self {
74		Self {
75			color: Some(0x6868a900),
76			..Default::default()
77		}
78	}
79	pub fn color(color: u32) -> Self {
80		Self {
81			color: Some(color),
82			..Default::default()
83		}
84	}
85	pub fn rgb([r, g, b]: [u8; 3]) -> Self {
86		Self::color(u32::from_be_bytes([r, g, b, 0]))
87	}
88
89	pub fn underline(mut self) -> Self {
90		self.underline = true;
91		self
92	}
93
94	pub fn bold(mut self) -> Self {
95		self.bold = true;
96		self
97	}
98
99	pub fn decoration(mut self) -> Self {
100		self.decoration = true;
101		self
102	}
103
104	// TODO: Use url crate for sanitization purposes?
105	pub fn url(mut self, url: String) -> Self {
106		self.url = Some(url);
107		self
108	}
109}
110
111const CSI: &str = "\x1b[";
112const OSC: &str = "\x1b]";
113const ST: &str = "\x1b\\";
114
115pub fn text_to_ansi(buf: &Text, out: &mut String) {
116	text_to_ansi_res(buf, out).expect("no fmt error")
117}
118pub fn text_to_ansi_res(buf: &Text, out: &mut String) -> fmt::Result {
119	use std::fmt::Write;
120
121	for (text, meta) in buf.fragments() {
122		if meta.bold {
123			write!(out, "{CSI}1m")?;
124		}
125		if meta.underline {
126			write!(out, "{CSI}4m")?;
127		}
128		if let Some(color) = meta.color {
129			let [r, g, b, _a] = u32::to_be_bytes(color);
130			write!(out, "{CSI}38;2;{r};{g};{b}m")?;
131		}
132		if let Some(bg_color) = meta.bg_color {
133			let [r, g, b, _a] = u32::to_be_bytes(bg_color);
134			write!(out, "{CSI}48;2;{r};{g};{b}m")?;
135		}
136		// We might want to add `id=` parameter to make terminals highlight split `Text`?
137		if let Some(url) = &meta.url {
138			write!(out, "{OSC}8;;{url}{ST}")?;
139		}
140		for chunk in text {
141			write!(out, "{chunk}")?;
142		}
143		if meta.url.is_some() {
144			write!(out, "{OSC}8;;{ST}")?;
145		}
146		if meta.color.is_some() || meta.bg_color.is_some() || meta.underline || meta.bold {
147			write!(out, "{CSI}0m")?;
148		}
149	}
150	Ok(())
151}