1use crate::style::{Style, StyleStack};
13use crate::text::Text;
14
15#[derive(Debug, Clone, PartialEq)]
21pub struct Tag {
22 pub name: String,
23 pub parameters: Option<String>,
24}
25
26impl Tag {
27 pub fn new(name: impl Into<String>) -> Self {
28 Self {
29 name: name.into(),
30 parameters: None,
31 }
32 }
33
34 pub fn with_params(name: impl Into<String>, params: impl Into<String>) -> Self {
35 Self {
36 name: name.into(),
37 parameters: Some(params.into()),
38 }
39 }
40
41 pub fn is_closing(&self) -> bool {
43 self.name == "/" || self.name.starts_with('/')
44 }
45
46 pub fn closing_name(&self) -> &str {
48 if self.name == "/" {
49 ""
50 } else {
51 &self.name[1..]
52 }
53 }
54
55 pub fn markup(&self) -> String {
57 if let Some(ref params) = self.parameters {
58 format!("[{}={}]", self.name, params)
59 } else {
60 format!("[{}]", self.name)
61 }
62 }
63}
64
65pub fn render(markup: &str) -> Text {
71 let mut text = Text::new("");
72 let mut style_stack = StyleStack::new(Style::new());
73 let mut pos = 0usize;
74
75 let chars: Vec<char> = markup.chars().collect();
76 let len = chars.len();
77
78 while pos < len {
79 if chars[pos] == '[' {
80 if pos + 1 < len && chars[pos + 1] == '[' {
82 text.append_styled("[", style_stack.current());
83 pos += 2;
84 continue;
85 }
86
87 let end = match chars[pos..].iter().position(|&c| c == ']') {
89 Some(e) => pos + e,
90 None => {
91 text.append_styled("[", style_stack.current());
93 pos += 1;
94 continue;
95 }
96 };
97
98 let tag_str: String = chars[pos + 1..end].iter().collect();
99 pos = end + 1;
100
101 if tag_str.is_empty() {
102 continue;
103 }
104
105 let tag = parse_tag(&tag_str);
107
108 if tag.is_closing() {
109 let closing = tag.closing_name();
110 if closing.is_empty() {
111 while style_stack.len() > 0 {
113 style_stack.pop();
114 }
115 } else {
116 style_stack.pop();
119 }
120 } else {
121 let style = tag_to_style(&tag);
123 style_stack.push(style);
124 }
125 } else {
126 let start = pos;
128 while pos < len && chars[pos] != '[' {
129 pos += 1;
130 }
131 let chunk: String = chars[start..pos].iter().collect();
132 text.append_styled(chunk, style_stack.current());
133 }
134 }
135
136 text
137}
138
139fn parse_tag(s: &str) -> Tag {
141 if s.starts_with('/') {
143 return Tag::new(s.to_string());
144 }
145
146 if let Some(eq) = s.find('=') {
148 let name = s[..eq].to_string();
149 let value = s[eq + 1..].to_string();
150 let value = value.trim_matches('"').trim_matches('\'').to_string();
152 return Tag::with_params(name, value);
153 }
154
155 if let Some(lparen) = s.find('(') {
157 if s.ends_with(')') {
158 let name = s[..lparen].to_string();
159 let params = s[lparen + 1..s.len() - 1].to_string();
160 return Tag::with_params(name, params);
161 }
162 }
163
164 Tag::new(s.to_string())
165}
166
167fn tag_to_style(tag: &Tag) -> Style {
169 let name = &tag.name;
170
171 match name.as_str() {
172 "bold" | "b" => Style::new().bold(true),
173 "dim" | "d" => Style::new().dim(true),
174 "italic" | "i" => Style::new().italic(true),
175 "underline" | "u" => Style::new().underline(true),
176 "blink" => Style::new().blink(true),
177 "reverse" | "r" => Style::new().reverse(true),
178 "strike" | "s" => Style::new().strike(true),
179
180 "/bold" | "/b" | "/dim" | "/d" | "/italic" | "/i"
181 | "/underline" | "/u" | "/blink" | "/reverse" | "/r"
182 | "/strike" | "/s" => Style::null(),
183
184 _ => {
185 if name.starts_with("on ") {
187 if let Ok(c) = crate::color::Color::parse(&name[3..]) {
188 return Style::new().bgcolor(c);
189 }
190 }
191
192 if let Some(on_pos) = name.find(" on ") {
194 let fg_name = &name[..on_pos];
195 let bg_name = &name[on_pos + 4..];
196 if let Ok(fg) = crate::color::Color::parse(fg_name) {
197 let mut style = Style::new().color(fg);
198 if let Ok(bg) = crate::color::Color::parse(bg_name) {
199 style = style.bgcolor(bg);
200 }
201 return style;
202 }
203 }
204
205 if let Ok(c) = crate::color::Color::parse(name) {
207 return Style::new().color(c);
208 }
209
210 if let Some(ref params) = tag.parameters {
212 if let Ok(c) = crate::color::Color::parse(params) {
213 return Style::new().color(c);
214 }
215 }
216
217 Style::new()
219 }
220 }
221}
222
223pub fn escape(markup: &str) -> String {
229 markup.replace('[', "[[")
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_escape() {
238 assert_eq!(escape("[bold]"), "[[bold]");
239 }
240
241 #[test]
242 fn test_render_bold() {
243 let t = render("[bold]Hello[/bold]");
244 assert_eq!(t.plain, "Hello");
245 assert!(!t.spans.is_empty()); }
247
248 #[test]
249 fn test_render_literal_bracket() {
250 let t = render("[[hello]]");
251 assert!(t.plain.contains("hello"));
253 }
254
255 #[test]
256 fn test_render_color() {
257 let t = render("[red]red text[/red]");
258 assert_eq!(t.plain, "red text");
259 assert!(!t.spans.is_empty());
260 }
261
262 #[test]
263 fn test_parse_tag() {
264 let tag = parse_tag("bold");
265 assert_eq!(tag.name, "bold");
266
267 let tag = parse_tag("color=red");
268 assert_eq!(tag.name, "color");
269 assert_eq!(tag.parameters, Some("red".into()));
270
271 let tag = parse_tag("/bold");
272 assert!(tag.is_closing());
273 }
274}