use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub enum ParamValue {
Str(String),
Num(f32),
Bool(bool),
}
impl std::fmt::Display for ParamValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParamValue::Str(s) => write!(f, "\"{}\"", s),
ParamValue::Num(n) => write!(f, "{}", n),
ParamValue::Bool(b) => write!(f, "{}", b),
}
}
}
#[derive(Clone)]
pub struct Flag {
name: String,
params: HashMap<String, ParamValue>,
content: String,
action: Option<Arc<dyn Fn(&str, &HashMap<String, ParamValue>, &str) + Send + Sync>>,
}
impl std::fmt::Debug for Flag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Flag")
.field("name", &self.name)
.field("params", &self.params)
.field("content", &self.content)
.field("action", &format_args!("<action closure>"))
.finish()
}
}
impl Flag {
pub fn new(name: String, params: HashMap<String, ParamValue>, content: String) -> Flag {
Flag {
name,
params,
content,
action: None,
}
}
pub fn with_name(name: String) -> Flag {
Flag {
name,
params: HashMap::new(),
content: String::new(),
action: None,
}
}
pub fn set_action<F>(&mut self, action: F)
where
F: Fn(&str, &HashMap<String, ParamValue>, &str) + Send + Sync + 'static,
{
self.action = Some(Arc::new(action));
}
pub fn execute_action(&self) {
if let Some(action) = &self.action {
action(&self.name, &self.params, &self.content);
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn params(&self) -> &HashMap<String, ParamValue> {
&self.params
}
pub fn content(&self) -> &str {
&self.content
}
}
#[derive(Debug, Clone)]
pub struct ParseOptions {
pub allow_nesting: bool,
pub ignore_code: bool,
pub ignore_latex: bool,
}
impl Default for ParseOptions {
fn default() -> Self {
ParseOptions {
allow_nesting: false,
ignore_code: true,
ignore_latex: true,
}
}
}
#[derive(Debug)]
pub struct Flagger {
flags: Vec<Flag>,
options: ParseOptions,
}
impl Flagger {
pub fn new(options: Option<ParseOptions>) -> Self {
Flagger {
flags: Vec::new(),
options: options.unwrap_or_default(),
}
}
fn parse(&mut self, text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut current_pos = 0;
let mut in_code_block = false;
let mut in_inline_code = false;
let mut in_latex_block = false;
let mut in_latex_inline = false;
let mut in_math_inline = false;
let chars: Vec<_> = text.char_indices().collect();
let chars_len = chars.len();
let mut i = 0;
while i < chars_len {
let (char_pos, c) = chars[i];
if c == '\\' && i + 1 < chars_len && chars[i + 1].1 == '<' {
result.push_str(&text[current_pos..char_pos]);
result.push('<');
current_pos = chars[i + 1].0 + '<'.len_utf8();
i += 2;
continue;
}
if self.options.ignore_code {
if i + 2 < chars_len && c == '`' && chars[i + 1].1 == '`' && chars[i + 2].1 == '`' {
in_code_block = !in_code_block;
i += 3;
continue;
}
if c == '`' && !in_code_block {
in_inline_code = !in_inline_code;
i += 1;
continue;
}
}
if (self.options.ignore_code && (in_code_block || in_inline_code)) ||
(self.options.ignore_latex && (in_latex_block || in_latex_inline || in_math_inline)) {
i += 1;
continue;
}
if c == '<' && i + 1 < chars_len && chars[i + 1].1 != '/' {
let mut j = i + 1;
while j < chars_len && chars[j].1 != '>' {
j += 1;
}
if j < chars_len {
let opening_tag_start = chars[i].0 + '<'.len_utf8();
let opening_tag_end = chars[j].0;
let opening_tag = &text[opening_tag_start..opening_tag_end];
let (flag_name, params) = self.parse_opening_tag(opening_tag);
let closing_tag = format!("</{}>", flag_name);
let rest_of_text_pos = chars[j].0 + '>'.len_utf8();
let rest_of_text = &text[rest_of_text_pos..];
if let Some(content_end_pos) = self.find_closing_tag(rest_of_text, &closing_tag) {
let content = &rest_of_text[0..content_end_pos];
let processed_content = if self.options.allow_nesting {
self.parse(content)
} else {
content.to_string()
};
let flag = Flag::new(flag_name, params, processed_content);
self.flags.push(flag.clone());
flag.execute_action();
result.push_str(&text[current_pos..char_pos]);
current_pos = rest_of_text_pos + content_end_pos + closing_tag.len();
i = 0;
while i < chars_len && chars[i].0 < current_pos {
i += 1;
}
continue;
}
}
}
i += 1;
}
if current_pos < text.len() {
result.push_str(&text[current_pos..]);
}
result
}
fn parse_opening_tag(&self, tag: &str) -> (String, HashMap<String, ParamValue>) {
let mut params = HashMap::new();
let parts: Vec<&str> = tag.splitn(2, char::is_whitespace).collect();
let flag_name = if parts.is_empty() { "" } else { parts[0] }.to_string();
if parts.len() < 2 {
return (flag_name, params);
}
let param_str = parts[1];
let mut chars = param_str.chars().peekable();
let mut param_name = String::new();
let mut param_value = String::new();
let mut in_quotes = false;
let mut quote_char = ' ';
let mut state = 0;
while let Some(c) = chars.next() {
match state {
0 => {
if c == '=' {
state = 1;
} else if !c.is_whitespace() {
param_name.push(c);
}
},
1 => {
if !in_quotes && c.is_whitespace() {
if !param_name.is_empty() {
params.insert(param_name.clone(), self.parse_param_value(¶m_value));
param_name.clear();
param_value.clear();
state = 0;
}
} else if !in_quotes && (c == '"' || c == '\'') {
in_quotes = true;
quote_char = c;
} else if in_quotes && c == quote_char {
params.insert(param_name.clone(), ParamValue::Str(param_value.clone()));
param_name.clear();
param_value.clear();
in_quotes = false;
state = 0;
} else {
param_value.push(c);
}
},
_ => {}
}
}
if !param_name.is_empty() && !param_value.is_empty() {
let value = if in_quotes {
ParamValue::Str(param_value)
} else {
self.parse_param_value(¶m_value)
};
params.insert(param_name, value);
}
(flag_name, params)
}
fn parse_param_value(&self, value: &str) -> ParamValue {
if (value.starts_with('"') && value.ends_with('"')) ||
(value.starts_with('\'') && value.ends_with('\'')) {
let inner = &value[1..value.len() - 1];
return ParamValue::Str(inner.to_string());
}
if let Ok(num) = value.parse::<f32>() {
return ParamValue::Num(num);
}
match value.to_lowercase().as_str() {
"true" => return ParamValue::Bool(true),
"false" => return ParamValue::Bool(false),
_ => {}
}
ParamValue::Str(value.to_string())
}
fn find_closing_tag(&self, text: &str, closing_tag: &str) -> Option<usize> {
let mut nesting_level = 0;
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '<' && i + 1 < chars.len() && chars[i + 1] != '/' {
if self.options.allow_nesting {
nesting_level += 1;
}
}
if i + closing_tag.len() <= text.len() && &text[i..i + closing_tag.len()] == closing_tag {
if nesting_level == 0 {
return Some(i);
}
nesting_level -= 1;
}
i += 1;
}
None
}
pub fn flags(&self) -> &[Flag] {
&self.flags
}
pub fn get_flag(&self, name: &str) -> Option<&Flag> {
self.flags.iter().find(|f| f.name() == name)
}
pub fn get_flags_by_name(&self, name: &str) -> Vec<&Flag> {
self.flags.iter().filter(|f| f.name() == name).collect()
}
pub fn register_action<F>(&mut self, flag_name: &str, action: F)
where
F: Fn(&str, &HashMap<String, ParamValue>, &str) + Send + Sync + 'static,
{
let action_arc = Arc::new(action);
for flag in self.flags.iter_mut() {
if flag.name() == flag_name {
flag.action = Some(action_arc.clone());
}
}
}
pub fn clear(&mut self) {
self.flags.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_flag_parsing() {
let mut flagger = Flagger::new(None);
let text = "This is a <test>content</test> with a flag.";
flagger.parse(text);
assert_eq!(flagger.flags().len(), 1);
assert_eq!(flagger.flags()[0].name(), "test");
assert_eq!(flagger.flags()[0].content(), "content");
}
#[test]
fn test_flag_with_parameters() {
let mut flagger = Flagger::new(None);
let text = "This is a <test param1=\"value1\" param2=42 param3=true>content</test> with parameters.";
flagger.parse(text);
assert_eq!(flagger.flags().len(), 1);
let flag = &flagger.flags()[0];
assert_eq!(flag.name(), "test");
assert_eq!(flag.content(), "content");
let params = flag.params();
assert_eq!(params.len(), 3);
assert_eq!(params.get("param1"), Some(&ParamValue::Str("value1".to_string())));
assert_eq!(params.get("param2"), Some(&ParamValue::Num(42.0)));
assert_eq!(params.get("param3"), Some(&ParamValue::Bool(true)));
}
#[test]
fn test_escaped_flag() {
let mut flagger = Flagger::new(None);
let text = "This is an escaped \\<test>content</test> flag.";
let result = flagger.parse(text);
assert_eq!(flagger.flags().len(), 0);
assert_eq!(result, "This is an escaped <test>content</test> flag.");
}
#[test]
fn test_code_block_ignoring() {
let mut flagger = Flagger::new(None);
let text = "This is outside. ```This is <test>content</test> in a code block.``` Back outside.";
flagger.parse(text);
assert_eq!(flagger.flags().len(), 0);
}
#[test]
fn test_latex_ignoring() {
let mut flagger = Flagger::new(None);
let text = "This is outside. $This is <test>content</test> in LaTeX.$ Back outside.";
flagger.parse(text);
assert_eq!(flagger.flags().len(), 0);
}
#[test]
fn test_action_execution() {
use std::sync::{Arc, Mutex};
let executed = Arc::new(Mutex::new(false));
let executed_clone = executed.clone();
let mut flagger = Flagger::new(None);
let text = "This is a <test>content</test> with action.";
flagger.parse(text);
flagger.register_action("test", move |name, params, content| {
*executed_clone.lock().unwrap() = true;
assert_eq!(name, "test");
assert_eq!(content, "content");
});
for flag in flagger.flags.iter() {
flag.execute_action();
}
assert!(*executed.lock().unwrap());
}
#[test]
fn test_quoted_string_parameters() {
let mut flagger = Flagger::new(None);
let text = "This is a <test param1=\"quoted value\" param2='single quoted'>content</test> with quotes.";
flagger.parse(text);
assert_eq!(flagger.flags().len(), 1);
let flag = &flagger.flags()[0];
let params = flag.params();
assert_eq!(params.get("param1"), Some(&ParamValue::Str("quoted value".to_string())));
assert_eq!(params.get("param2"), Some(&ParamValue::Str("single quoted".to_string())));
}
}