1use std::{fmt, io, str};
4
5use termcolor::{Color, ColorSpec};
6use unicode_width::UnicodeWidthChar;
7
8mod html;
9#[cfg(feature = "svg")]
10mod svg;
11#[cfg(test)]
12mod tests;
13
14pub(crate) use self::html::HtmlWriter;
15#[cfg(feature = "svg")]
16pub(crate) use self::svg::{SvgLine, SvgWriter};
17
18fn fmt_to_io_error(err: fmt::Error) -> io::Error {
19 io::Error::other(err)
20}
21
22#[derive(Debug)]
24struct StyledSpan {
25 classes: Vec<String>,
26 styles: Vec<String>,
27}
28
29impl StyledSpan {
30 fn new(spec: &ColorSpec, fg_property: &str) -> io::Result<Self> {
31 let mut classes = vec![];
32 if spec.bold() {
33 classes.push("bold".to_owned());
34 }
35 if spec.dimmed() {
36 classes.push("dimmed".to_owned());
37 }
38 if spec.italic() {
39 classes.push("italic".to_owned());
40 }
41 if spec.underline() {
42 classes.push("underline".to_owned());
43 }
44
45 let mut this = Self {
46 classes,
47 styles: vec![],
48 };
49 if let Some(color) = spec.fg() {
50 let color = IndexOrRgb::new(*color)?;
51 this.set_fg(color, spec.intense(), &[fg_property]);
52 }
53 Ok(this)
54 }
55
56 fn set_fg(&mut self, color: IndexOrRgb, intense: bool, fg_properties: &[&str]) {
57 use fmt::Write as _;
58
59 let mut fore_color_class = String::with_capacity(4);
60 fore_color_class.push_str("fg");
61 match color {
62 IndexOrRgb::Index(idx) => {
63 let final_idx = if intense { idx | 8 } else { idx };
64 write!(&mut fore_color_class, "{final_idx}").unwrap();
65 self.classes.push(fore_color_class);
67 }
68 IndexOrRgb::Rgb(r, g, b) => {
69 for property in fg_properties {
70 self.styles
71 .push(format!("{property}: #{r:02x}{g:02x}{b:02x}"));
72 }
73 }
74 }
75 }
76
77 fn write_tag(self, output: &mut impl WriteStr, tag: &str) -> io::Result<()> {
78 output.write_str("<")?;
79 output.write_str(tag)?;
80 if !self.classes.is_empty() {
81 output.write_str(" class=\"")?;
82 for (i, class) in self.classes.iter().enumerate() {
83 output.write_str(class)?;
84 if i + 1 < self.classes.len() {
85 output.write_str(" ")?;
86 }
87 }
88 output.write_str("\"")?;
89 }
90 if !self.styles.is_empty() {
91 output.write_str(" style=\"")?;
92 for (i, style) in self.styles.iter().enumerate() {
93 output.write_str(style)?;
94 if i + 1 < self.styles.len() {
95 output.write_str("; ")?;
96 }
97 }
98 output.write_str(";\"")?;
99 }
100 output.write_str(">")
101 }
102}
103
104trait WriteStr {
106 fn write_str(&mut self, s: &str) -> io::Result<()>;
107}
108
109impl WriteStr for String {
110 fn write_str(&mut self, s: &str) -> io::Result<()> {
111 <Self as fmt::Write>::write_str(self, s).map_err(fmt_to_io_error)
112 }
113}
114
115trait WriteLines: WriteStr {
117 fn line_splitter_mut(&mut self) -> Option<&mut LineSplitter>;
118
119 fn write_line_break(&mut self, br: LineBreak, char_width: usize) -> io::Result<()>;
122
123 fn write_new_line(&mut self, char_width: usize) -> io::Result<()>;
125
126 #[allow(clippy::option_if_let_else)] fn write_text(&mut self, s: &str) -> io::Result<()> {
129 if let Some(splitter) = self.line_splitter_mut() {
130 let lines = splitter.split_lines(s);
131 self.write_lines(lines)
132 } else {
133 self.write_str(s)
134 }
135 }
136
137 fn write_lines(&mut self, lines: Vec<Line<'_>>) -> io::Result<()> {
138 let lines_count = lines.len();
139 let it = lines.into_iter().enumerate();
140 for (i, line) in it {
141 self.write_str(line.text)?;
142 if let Some(br) = line.br {
143 self.write_line_break(br, line.char_width)?;
144 } else if i + 1 < lines_count {
145 self.write_new_line(line.char_width)?;
146 }
147 }
148 Ok(())
149 }
150
151 #[allow(clippy::option_if_let_else)] fn write_html_entity(&mut self, entity: &str) -> io::Result<()> {
154 if let Some(splitter) = self.line_splitter_mut() {
155 let lines = splitter.write_as_char(entity);
156 self.write_lines(lines)
157 } else {
158 self.write_str(entity)
159 }
160 }
161
162 fn io_write(&mut self, buffer: &[u8], convert_spaces: bool) -> io::Result<usize> {
164 let mut last_escape = 0;
165 for (i, &byte) in buffer.iter().enumerate() {
166 let escaped = match byte {
167 b'>' => ">",
168 b'<' => "<",
169 b'&' => "&",
170 b' ' if convert_spaces => "\u{a0}", _ => continue,
172 };
173 let saved_str = str::from_utf8(&buffer[last_escape..i])
174 .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
175 self.write_text(saved_str)?;
176 self.write_html_entity(escaped)?;
177 last_escape = i + 1;
178 }
179
180 let saved_str = str::from_utf8(&buffer[last_escape..])
181 .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
182 self.write_text(saved_str)?;
183 Ok(buffer.len())
184 }
185}
186
187#[derive(Debug, Clone, Copy)]
188pub(crate) enum IndexOrRgb {
189 Index(u8),
190 Rgb(u8, u8, u8),
191}
192
193impl IndexOrRgb {
194 #[allow(clippy::match_wildcard_for_single_variants)]
195 fn new(color: Color) -> io::Result<Self> {
197 Ok(match color {
198 Color::Black => Self::index(0),
199 Color::Red => Self::index(1),
200 Color::Green => Self::index(2),
201 Color::Yellow => Self::index(3),
202 Color::Blue => Self::index(4),
203 Color::Magenta => Self::index(5),
204 Color::Cyan => Self::index(6),
205 Color::White => Self::index(7),
206 Color::Ansi256(idx) => Self::indexed_color(idx),
207 Color::Rgb(r, g, b) => Self::Rgb(r, g, b),
208 _ => return Err(io::Error::other("Unsupported color")),
209 })
210 }
211
212 fn index(value: u8) -> Self {
213 debug_assert!(value < 16);
214 Self::Index(value)
215 }
216
217 pub fn indexed_color(index: u8) -> Self {
218 match index {
219 0..=15 => Self::index(index),
220
221 16..=231 => {
222 let index = index - 16;
223 let r = Self::color_cube_color(index / 36);
224 let g = Self::color_cube_color((index / 6) % 6);
225 let b = Self::color_cube_color(index % 6);
226 Self::Rgb(r, g, b)
227 }
228
229 _ => {
230 let gray = 10 * (index - 232) + 8;
231 Self::Rgb(gray, gray, gray)
232 }
233 }
234 }
235
236 fn color_cube_color(index: u8) -> u8 {
237 match index {
238 0 => 0,
239 1 => 0x5f,
240 2 => 0x87,
241 3 => 0xaf,
242 4 => 0xd7,
243 5 => 0xff,
244 _ => unreachable!(),
245 }
246 }
247}
248
249#[derive(Debug)]
250struct LineSplitter {
251 max_width: usize,
252 current_width: usize,
253}
254
255impl Default for LineSplitter {
256 fn default() -> Self {
257 Self {
258 max_width: usize::MAX,
259 current_width: 0,
260 }
261 }
262}
263
264impl LineSplitter {
265 fn new(max_width: usize) -> Self {
266 Self {
267 max_width,
268 current_width: 0,
269 }
270 }
271
272 fn split_lines<'a>(&mut self, text: &'a str) -> Vec<Line<'a>> {
273 text.lines()
274 .chain(if text.ends_with('\n') { Some("") } else { None })
275 .enumerate()
276 .flat_map(|(i, line)| {
277 if i > 0 {
278 self.current_width = 0;
279 }
280 self.process_line(line)
281 })
282 .collect()
283 }
284
285 fn write_as_char<'a>(&mut self, text: &'a str) -> Vec<Line<'a>> {
286 if self.current_width + 1 > self.max_width {
287 let char_width = self.current_width;
288 self.current_width = 1;
289 vec![
290 Line {
291 text: "",
292 br: Some(LineBreak::Hard),
293 char_width,
294 },
295 Line {
296 text,
297 br: None,
298 char_width: 1,
299 },
300 ]
301 } else {
302 self.current_width += 1;
303 vec![Line {
304 text,
305 br: None,
306 char_width: self.current_width,
307 }]
308 }
309 }
310
311 fn process_line<'a>(&mut self, line: &'a str) -> Vec<Line<'a>> {
312 let mut output_lines = vec![];
313 let mut line_start = 0;
314
315 for (pos, char) in line.char_indices() {
316 let char_width = char.width().unwrap_or(0);
317 if self.current_width + char_width > self.max_width {
318 output_lines.push(Line {
319 text: &line[line_start..pos],
320 br: Some(LineBreak::Hard),
321 char_width: self.current_width,
322 });
323 line_start = pos;
324 self.current_width = char_width;
325 } else {
326 self.current_width += char_width;
327 }
328 }
329
330 output_lines.push(Line {
331 text: &line[line_start..],
332 br: None,
333 char_width: self.current_width,
334 });
335 output_lines
336 }
337}
338
339#[derive(Debug, PartialEq)]
340struct Line<'a> {
341 text: &'a str,
342 br: Option<LineBreak>,
343 char_width: usize,
344}
345
346#[derive(Debug, Clone, Copy, PartialEq)]
347enum LineBreak {
348 Hard,
349}