use ansi_term::Style;
use lazy_static::lazy_static;
use regex::Regex;
use super::outputstyle::OutputStyle;
lazy_static! {
static ref RE_MONOSPACE: Regex = Regex::new(r"`([^`]*?)`").unwrap();
static ref RE_MONOSPACE_OLD: Regex = Regex::new(r"```([^`]*?)```").unwrap();
static ref RE_MONOSPACE_TRIM: Regex = Regex::new(r"\s*`(?: *\n)?([^`]+?)\n?`\s*").unwrap();
static ref RE_BACKTICK: Regex = Regex::new(r"(`[^`]+`)|([^`]+)").unwrap();
static ref RE_ALL_BUT_MONOSPACE: Regex =
Regex::new(r"\[\[((?s).*?)\]\]|\{\{((?s).*?)\}\}|<<((?s).*?)>>").unwrap();
static ref RE_SPACES: Regex = Regex::new(r" +").unwrap();
static ref RE_NONWHITESPACE: Regex = Regex::new(r"[^\r\n ]+").unwrap();
static ref RE_NEWLINES: Regex = Regex::new(r"\n\n\n+").unwrap();
}
pub fn format_cg(text: &str, ostyle: &OutputStyle) -> String {
if RE_MONOSPACE_OLD.is_match(text) {
eprintln!(
"{} Clash contains obsolete ``` formatting, consider fixing it in the website.\n",
ostyle.failure.paint("WARNING"),
);
}
let mut text = format_edit_monospace(text);
text = format_trim_consecutive_spaces(&text);
text = format_monospace_padding(&text);
text = format_paint(&text, ostyle);
format_remove_excessive_newlines(&text)
}
fn format_edit_monospace(text: &str) -> String {
let mut result = text.replace("```", "`");
result = RE_MONOSPACE_TRIM
.replace_all(&result, |caps: ®ex::Captures| format!("\n\n`{}`\n\n", &caps[1]))
.to_string();
result
}
fn format_trim_consecutive_spaces(text: &str) -> String {
RE_BACKTICK
.replace_all(text, |caps: ®ex::Captures| {
if let Some(monospace_text) = caps.get(1) {
monospace_text.as_str().to_string()
} else if let Some(non_monospace_text) = caps.get(2) {
RE_SPACES.replace_all(non_monospace_text.as_str(), " ").to_string()
} else {
"".to_string()
}
})
.to_string()
}
fn format_monospace_padding(text: &str) -> String {
RE_MONOSPACE
.replace_all(text, |caps: ®ex::Captures| {
let lines: Vec<&str> = caps[1].split('\n').collect();
let padding = lines.iter().map(|line| clean_line_size(line)).max().unwrap_or(0);
let formatted_lines = lines
.iter()
.map(|&line| {
let offset = line.len() - clean_line_size(line);
format!("`{:<width$}`", line, width = padding + offset)
})
.collect::<Vec<String>>()
.join("\n");
formatted_lines
})
.to_string()
}
fn clean_line_size(line: &str) -> usize {
let amount_tag_blocks: usize = RE_ALL_BUT_MONOSPACE.find_iter(line).count();
line.len() - 4 * amount_tag_blocks
}
fn paint_parts<'a>(text: &'a str, style_tag_pairs: &[(Style, &str, &str)]) -> Vec<ansi_term::ANSIString<'a>> {
let mut parts = Vec::<ansi_term::ANSIString<'a>>::new();
let mut cur_style = Style::default();
let mut buffer = String::new();
let mut skip_until = 0;
let mut num_warnings = 0;
let mut stack: Vec<(Style, &str)> = vec![];
for (i, c) in text.char_indices() {
if i < skip_until {
continue;
}
let slice = &text[i..];
for (style, tag_open, tag_close) in style_tag_pairs {
if slice.starts_with(tag_close) {
if let Some((style, opening)) = stack.to_owned().last() {
if opening == tag_open {
stack.pop();
parts.push(cur_style.paint(buffer.to_string()));
buffer.clear();
cur_style = *style;
skip_until = i + tag_close.len();
break
} else {
if num_warnings == 0 {
eprintln!(
"{} Bad formatting: tried to close {:?} with {:?}",
Style::new().on(ansi_term::Color::Red).paint("WARNING"),
opening,
tag_close,
);
}
num_warnings += 1;
}
}
}
if slice.starts_with(tag_open) {
if slice.contains(tag_close) {
parts.push(cur_style.paint(buffer.to_owned()));
buffer.clear();
stack.push((cur_style, tag_open));
cur_style = nested_style(style, &cur_style);
skip_until = i + tag_open.len();
} else {
if num_warnings == 0 {
eprintln!(
"{} Bad formatting: ignoring {:?} that is never closed",
Style::new().on(ansi_term::Color::Red).paint("WARNING"),
tag_open
);
}
num_warnings += 1;
}
break
}
}
if i >= skip_until {
buffer.push(c);
}
}
for (_, tag_open) in stack {
if num_warnings == 0 {
eprintln!(
"{} Bad formatting: {:?} was never closed",
Style::new().on(ansi_term::Color::Red).paint("WARNING"),
tag_open
);
}
num_warnings += 1;
}
if !buffer.is_empty() {
parts.push(cur_style.paint(buffer.to_string()));
}
parts
}
fn format_paint(text: &str, ostyle: &OutputStyle) -> String {
let tag_pairs = vec![
(ostyle.monospace, "`", "`"),
(ostyle.variable, "[[", "]]"),
(ostyle.constant, "{{", "}}"),
(ostyle.bold, "<<", ">>"),
];
let parts = paint_parts(text, &tag_pairs);
ansi_term::ANSIStrings(&parts).to_string()
}
fn format_remove_excessive_newlines(text: &str) -> String {
RE_NEWLINES.replace_all(text, |_: ®ex::Captures| "\n\n").trim_end().to_string()
}
pub fn show_whitespace(text: &str, style: &Style, ws_style: &Option<Style>) -> String {
match ws_style {
None => style.paint(text).to_string(),
Some(ws_style) => {
let newl = format!("{}\n", ws_style.paint("⏎"));
let space = format!("{}", ws_style.paint("•"));
let fmt_non_ws = RE_NONWHITESPACE
.replace_all(text, |caps: ®ex::Captures| style.paint(&caps[0]).to_string())
.to_string();
fmt_non_ws.replace('\n', &newl).replace(' ', &space)
}
}
}
fn nested_style(inner: &Style, outer: &Style) -> Style {
Style {
foreground: inner.foreground.or(outer.foreground),
background: inner.background.or(outer.background),
is_bold: inner.is_bold || outer.is_bold,
is_italic: inner.is_italic || outer.is_italic,
is_underline: inner.is_underline || outer.is_underline,
is_blink: inner.is_blink || outer.is_blink,
is_dimmed: inner.is_dimmed || outer.is_dimmed,
is_reverse: inner.is_reverse || outer.is_reverse,
is_hidden: inner.is_hidden || outer.is_hidden,
is_strikethrough: inner.is_strikethrough || outer.is_strikethrough,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trim_spaces_with_format() {
let text = "hello world";
assert_eq!(format_trim_consecutive_spaces(text), "hello world");
}
#[test]
fn does_not_trim_spaces_in_monospace() {
let text = "`{\n let x = 5;\n}`";
assert!(format_trim_consecutive_spaces(text).contains("{\n let x = 5;\n}"));
}
#[test]
fn format_monospace_coloring_removes_backticks() {
let text = "To create a new variable use `let x = 5`";
let formatted_text = format_paint(text, &OutputStyle::default());
assert!(!formatted_text.contains('`'));
}
#[test]
fn format_monospace_adds_newline_if_there_is_none() {
let text = "I have `no whitespace`";
let formatted_text = format_edit_monospace(text);
assert!(formatted_text.contains('\n'));
}
#[test]
fn format_monospace_trims_trailing_spaces() {
let text = "I have `no whitespace` and more text";
let formatted_text = format_edit_monospace(text);
assert!(!formatted_text.contains("\n "));
}
#[test]
fn format_monospace_more_newlines_1() {
let text: &str = "1text `mono line` text";
let formatted_text = format_edit_monospace(text);
let expected = "1text\n\n`mono line`\n\ntext";
assert_eq!(formatted_text, expected);
}
#[test]
fn format_monospace_more_newlines_2() {
let text: &str = "2text \n\n`mono line\nnew line` \n text";
let formatted_text = format_edit_monospace(text);
let expected = "2text\n\n`mono line\nnew line`\n\ntext";
assert_eq!(formatted_text, expected);
}
#[test]
fn format_monospace_more_newlines_3() {
let text: &str = "3text \n\n \n `\n \n mono line\nnew line \n \n` \n \n text";
let formatted_text = format_edit_monospace(text);
let expected = "3text\n\n` \n mono line\nnew line \n `\n\ntext";
assert_eq!(formatted_text, expected);
}
#[test]
fn format_monospace_more_newlines_4() {
let text: &str = "4text\n\n`mono line`\n\ntext";
let formatted_text = format_edit_monospace(text);
let expected = "4text\n\n`mono line`\n\ntext";
assert_eq!(formatted_text, expected);
}
#[test]
fn format_deals_with_newspaces() {
let text = "Text with many\n\n\n\n\nnewlines\n\n";
let formatted_text = format_remove_excessive_newlines(text);
let expected = "Text with many\n\nnewlines";
assert_eq!(formatted_text, expected);
}
#[test]
fn painting_simple() {
use ansi_term::Color::*;
let red = Style::default().fg(Red);
let green = Style::default().fg(Green);
let blue = Style::default().fg(Blue);
let tag_pairs = vec![
(Style::default(), "{{", "}}"),
(blue, "[[", "]]"),
(red, "<<", ">>"),
(green, "`", "`"),
];
let parts = paint_parts("vv<<RED>>ww`GREEN`xx[[BLUE]]yy{{DEFAULT}}zz", &tag_pairs);
println!("\n{}", ansi_term::ANSIStrings(&parts));
assert_eq!(parts[0], ansi_term::ANSIString::from("vv"));
assert_eq!(parts[1], red.paint("RED"));
assert_eq!(parts[2], ansi_term::ANSIString::from("ww"));
assert_eq!(parts[3], green.paint("GREEN"));
assert_eq!(parts[4], ansi_term::ANSIString::from("xx"));
assert_eq!(parts[5], blue.paint("BLUE"));
assert_eq!(parts[6], ansi_term::ANSIString::from("yy"));
assert_eq!(parts[7], ansi_term::ANSIString::from("DEFAULT"));
assert_eq!(parts[8], ansi_term::ANSIString::from("zz"));
assert_eq!(parts.len(), 9);
}
#[test]
fn painting_nested() {
use ansi_term::Color::{Blue, Red};
let inner_style = Style::default().fg(Red);
let outer_style = Style::default().on(Blue);
let tag_pairs = vec![(outer_style, "`", "`"), (inner_style, "<<", ">>")];
let parts = paint_parts("AA`BB<<CC>>DD`EE", &tag_pairs);
println!("\n{}", ansi_term::ANSIStrings(&parts));
assert_eq!(parts[0], ansi_term::ANSIString::from("AA"));
assert_eq!(parts[1], outer_style.paint("BB"));
assert_eq!(parts[2], inner_style.on(Blue).paint("CC"));
assert_eq!(parts[3], outer_style.paint("DD"));
assert_eq!(parts[4], ansi_term::ANSIString::from("EE"));
assert_eq!(parts.len(), 5);
}
#[test]
fn painting_weird_and_invalid() {
let ostyle = OutputStyle::default();
println!("\nInvalid formatting tests:");
let examples = [
"<<AA[[BB>>CC]]",
"```",
"XX```YY",
"<<]]",
"[[[[AA]]",
"[[[[AA]]]]",
"<<[[AA>>]]>>",
];
for (idx, original) in examples.iter().enumerate() {
let formatted = format_paint(original, &ostyle);
println!(" {}. {:?} becomes \"{}\"", idx + 1, original, formatted);
}
}
}