|
1 | 1 | use ansi_escapers::{interpreter::*, types::*}; |
2 | 2 |
|
| 3 | +/// Escapes HTML special characters to prevent XSS attacks |
| 4 | +fn escape_html(s: &str) -> String { |
| 5 | + s.chars() |
| 6 | + .map(|c| match c { |
| 7 | + '&' => "&".to_string(), |
| 8 | + '<' => "<".to_string(), |
| 9 | + '>' => ">".to_string(), |
| 10 | + '"' => """.to_string(), |
| 11 | + '\'' => "'".to_string(), |
| 12 | + _ => c.to_string(), |
| 13 | + }) |
| 14 | + .collect() |
| 15 | +} |
| 16 | + |
3 | 17 | // ANSI color constants |
4 | 18 | const COLOR_BLACK: &str = "#000000"; |
5 | 19 | const COLOR_RED: &str = "#FF0000"; |
@@ -96,27 +110,72 @@ fn get_html_style(codes: Vec<SgrAttribute>) -> String { |
96 | 110 | } |
97 | 111 |
|
98 | 112 | pub fn ansi_to_html(inp: &str) -> String { |
99 | | - let mut interpreter = AnsiParser::new(inp); |
| 113 | + // Pre-process input to preserve newlines before ANSI parsing |
| 114 | + let inp = inp.replace("\n", "\\n").replace("\r", "\\r"); |
| 115 | + |
| 116 | + let mut interpreter = AnsiParser::new(&inp); |
100 | 117 | let parse_result = interpreter.parse_annotated(); |
101 | | - parse_result |
102 | | - .spans |
103 | | - .iter() |
104 | | - .map(|span| -> String { |
105 | | - if span.end > parse_result.text.len() { |
106 | | - return "".to_string(); |
107 | | - } |
108 | | - let mut res: String = String::new(); |
109 | | - res += format!( |
110 | | - "<span style=\"{}\">{}</span>", |
111 | | - get_html_style(span.codes.clone()), |
112 | | - &parse_result.text[span.start..span.end] |
113 | | - ) |
114 | | - .as_str(); |
115 | | - |
116 | | - res |
117 | | - }) |
118 | | - .filter(|x| !x.is_empty()) |
119 | | - //.map(|x| x.clone()) |
120 | | - .collect::<Vec<String>>() |
121 | | - .join("") |
| 118 | + |
| 119 | + // If there are no spans, just return the escaped text |
| 120 | + if parse_result.spans.is_empty() { |
| 121 | + // Restore newlines in the output |
| 122 | + return escape_html(&parse_result.text) |
| 123 | + .replace("\\n", "<br>") |
| 124 | + .replace("\\r", ""); |
| 125 | + } |
| 126 | + |
| 127 | + // Create styled spans |
| 128 | + let mut styled_spans = Vec::new(); |
| 129 | + for span in &parse_result.spans { |
| 130 | + if span.end > parse_result.text.len() { |
| 131 | + continue; |
| 132 | + } |
| 133 | + |
| 134 | + let style = get_html_style(span.codes.clone()); |
| 135 | + // Escape HTML and preserve newlines by converting to <br> |
| 136 | + let content = escape_html(&parse_result.text[span.start..span.end]) |
| 137 | + .replace("\\n", "<br>") |
| 138 | + .replace("\\r", ""); |
| 139 | + |
| 140 | + styled_spans.push(( |
| 141 | + span.start, |
| 142 | + span.end, |
| 143 | + format!("<span style=\"{}\">{}</span>", style, content), |
| 144 | + )); |
| 145 | + } |
| 146 | + |
| 147 | + // Sort spans by start position |
| 148 | + styled_spans.sort_by_key(|&(start, _, _)| start); |
| 149 | + |
| 150 | + // Build the final output by replacing text with styled spans |
| 151 | + let mut result = String::new(); |
| 152 | + let mut current_pos = 0; |
| 153 | + |
| 154 | + for (start, end, styled_span) in styled_spans { |
| 155 | + // Add any text before this span |
| 156 | + if start > current_pos { |
| 157 | + result.push_str( |
| 158 | + &escape_html(&parse_result.text[current_pos..start]) |
| 159 | + .replace("\\n", "<br>") |
| 160 | + .replace("\\r", ""), |
| 161 | + ); |
| 162 | + } |
| 163 | + |
| 164 | + // Add the styled span |
| 165 | + result.push_str(&styled_span); |
| 166 | + |
| 167 | + // Update current position |
| 168 | + current_pos = end; |
| 169 | + } |
| 170 | + |
| 171 | + // Add any remaining text after the last span |
| 172 | + if current_pos < parse_result.text.len() { |
| 173 | + result.push_str( |
| 174 | + &escape_html(&parse_result.text[current_pos..]) |
| 175 | + .replace("\\n", "<br>") |
| 176 | + .replace("\\r", ""), |
| 177 | + ); |
| 178 | + } |
| 179 | + |
| 180 | + result |
122 | 181 | } |
0 commit comments