use crate::style::{Style, StyleStack};
use crate::text::Text;
#[derive(Debug, Clone, PartialEq)]
pub struct Tag {
pub name: String,
pub parameters: Option<String>,
}
impl Tag {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
parameters: None,
}
}
pub fn with_params(name: impl Into<String>, params: impl Into<String>) -> Self {
Self {
name: name.into(),
parameters: Some(params.into()),
}
}
pub fn is_closing(&self) -> bool {
self.name == "/" || self.name.starts_with('/')
}
pub fn closing_name(&self) -> &str {
if self.name == "/" {
""
} else {
&self.name[1..]
}
}
pub fn markup(&self) -> String {
if let Some(ref params) = self.parameters {
format!("[{}={}]", self.name, params)
} else {
format!("[{}]", self.name)
}
}
}
pub fn render(markup: &str) -> Text {
let mut text = Text::new("");
let mut style_stack = StyleStack::new(Style::new());
let mut pos = 0usize;
let chars: Vec<char> = markup.chars().collect();
let len = chars.len();
while pos < len {
if chars[pos] == '[' {
if pos + 1 < len && chars[pos + 1] == '[' {
text.append_styled("[", style_stack.current());
pos += 2;
continue;
}
let end = match chars[pos..].iter().position(|&c| c == ']') {
Some(e) => pos + e,
None => {
text.append_styled("[", style_stack.current());
pos += 1;
continue;
}
};
let tag_str: String = chars[pos + 1..end].iter().collect();
pos = end + 1;
if tag_str.is_empty() {
continue;
}
let tag = parse_tag(&tag_str);
if tag.is_closing() {
let closing = tag.closing_name();
if closing.is_empty() {
while style_stack.len() > 0 {
style_stack.pop();
}
} else {
style_stack.pop();
}
} else {
let style = tag_to_style(&tag);
style_stack.push(style);
}
} else {
let start = pos;
while pos < len && chars[pos] != '[' {
pos += 1;
}
let chunk: String = chars[start..pos].iter().collect();
text.append_styled(chunk, style_stack.current());
}
}
text
}
fn parse_tag(s: &str) -> Tag {
if s.starts_with('/') {
return Tag::new(s.to_string());
}
if let Some(eq) = s.find('=') {
let name = s[..eq].to_string();
let value = s[eq + 1..].to_string();
let value = value.trim_matches('"').trim_matches('\'').to_string();
return Tag::with_params(name, value);
}
if let Some(lparen) = s.find('(') {
if s.ends_with(')') {
let name = s[..lparen].to_string();
let params = s[lparen + 1..s.len() - 1].to_string();
return Tag::with_params(name, params);
}
}
Tag::new(s.to_string())
}
fn tag_to_style(tag: &Tag) -> Style {
let name = &tag.name;
match name.as_str() {
"bold" | "b" => Style::new().bold(true),
"dim" | "d" => Style::new().dim(true),
"italic" | "i" => Style::new().italic(true),
"underline" | "u" => Style::new().underline(true),
"blink" => Style::new().blink(true),
"reverse" | "r" => Style::new().reverse(true),
"strike" | "s" => Style::new().strike(true),
"/bold" | "/b" | "/dim" | "/d" | "/italic" | "/i"
| "/underline" | "/u" | "/blink" | "/reverse" | "/r"
| "/strike" | "/s" => Style::null(),
_ => {
if name.starts_with("on ") {
if let Ok(c) = crate::color::Color::parse(&name[3..]) {
return Style::new().bgcolor(c);
}
}
if let Some(on_pos) = name.find(" on ") {
let fg_name = &name[..on_pos];
let bg_name = &name[on_pos + 4..];
if let Ok(fg) = crate::color::Color::parse(fg_name) {
let mut style = Style::new().color(fg);
if let Ok(bg) = crate::color::Color::parse(bg_name) {
style = style.bgcolor(bg);
}
return style;
}
}
if let Ok(c) = crate::color::Color::parse(name) {
return Style::new().color(c);
}
if let Some(ref params) = tag.parameters {
if let Ok(c) = crate::color::Color::parse(params) {
return Style::new().color(c);
}
}
Style::new()
}
}
}
pub fn escape(markup: &str) -> String {
markup.replace('[', "[[")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape() {
assert_eq!(escape("[bold]"), "[[bold]");
}
#[test]
fn test_render_bold() {
let t = render("[bold]Hello[/bold]");
assert_eq!(t.plain, "Hello");
assert!(!t.spans.is_empty()); }
#[test]
fn test_render_literal_bracket() {
let t = render("[[hello]]");
assert!(t.plain.contains("hello"));
}
#[test]
fn test_render_color() {
let t = render("[red]red text[/red]");
assert_eq!(t.plain, "red text");
assert!(!t.spans.is_empty());
}
#[test]
fn test_parse_tag() {
let tag = parse_tag("bold");
assert_eq!(tag.name, "bold");
let tag = parse_tag("color=red");
assert_eq!(tag.name, "color");
assert_eq!(tag.parameters, Some("red".into()));
let tag = parse_tag("/bold");
assert!(tag.is_closing());
}
}