use base64::{Engine as _, engine::general_purpose};
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use crate::extensions::plugins::block::render_fence_block_plugin;
pub fn protect_fence_block_plugins(input: &str) -> String {
static START_LINE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^:::(\w+)(?:[ \t]+([^\r\n]*))?[ \t]*\r?\n").unwrap());
static CLOSE_LINE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^:::[ \t]*(?:\r?\n|$)").unwrap());
let mut result = String::with_capacity(input.len());
let mut pos = 0usize;
while let Some(start_caps) = START_LINE.captures_at(input, pos) {
let start_match = start_caps.get(0).unwrap();
result.push_str(&input[pos..start_match.start()]);
let function = start_caps.get(1).map_or("", |m| m.as_str());
let args = start_caps.get(2).map_or("", |m| m.as_str()).trim();
let content_start = start_match.end();
match CLOSE_LINE.captures_at(input, content_start) {
Some(close_caps) => {
let close_match = close_caps.get(0).unwrap();
let raw_content = &input[content_start..close_match.start()];
let content = raw_content
.strip_suffix("\r\n")
.or_else(|| raw_content.strip_suffix('\n'))
.unwrap_or(raw_content);
let encoded_content = general_purpose::STANDARD.encode(content.as_bytes());
result.push_str(&format!(
"{{{{FENCE_BLOCK_PLUGIN:{}:{}:{}:FENCE_BLOCK_PLUGIN}}}}",
function, args, encoded_content
));
pos = close_match.end();
}
None => {
result.push_str(&input[start_match.start()..start_match.end()]);
pos = start_match.end();
}
}
}
result.push_str(&input[pos..]);
result
}
pub(crate) fn restore_markers(html: &str) -> String {
static FENCE_BLOCK_PLUGIN_MARKER: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"\{\{FENCE_BLOCK_PLUGIN:(\w+):([\s\S]*?):([\s\S]*?):FENCE_BLOCK_PLUGIN\}\}")
.unwrap()
});
FENCE_BLOCK_PLUGIN_MARKER
.replace_all(html, |caps: &Captures| {
let function = &caps[1];
let args = &caps[2];
let encoded_content = &caps[3];
let content = general_purpose::STANDARD
.decode(encoded_content.as_bytes())
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
.unwrap_or_else(|| encoded_content.to_string());
render_fence_block_plugin(function, args, &content)
})
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_protect_fence_block_plugin_basic() {
let input = ":::alert warning\nSomething went wrong\n:::";
let output = protect_fence_block_plugins(input);
assert!(output.contains("FENCE_BLOCK_PLUGIN:alert:warning:"));
assert!(!output.contains(":::"));
}
#[test]
fn test_protect_fence_block_plugin_no_args() {
let input = ":::toc\nignored\n:::";
let output = protect_fence_block_plugins(input);
assert!(output.contains("FENCE_BLOCK_PLUGIN:toc::"));
}
#[test]
fn test_protect_fence_block_plugin_empty_content() {
let input = ":::clear\n:::";
let output = protect_fence_block_plugins(input);
assert!(output.contains("FENCE_BLOCK_PLUGIN:clear::"));
assert!(output.contains("FENCE_BLOCK_PLUGIN}}"));
}
#[test]
fn test_protect_fence_block_plugin_multiline_content() {
let input = ":::box\nline one\nline two\n:::";
let output = protect_fence_block_plugins(input);
let marker_re = Regex::new(r"FENCE_BLOCK_PLUGIN:box::([^:]+):FENCE_BLOCK_PLUGIN").unwrap();
let caps = marker_re
.captures(&output)
.expect("marker should be present");
let decoded = general_purpose::STANDARD.decode(&caps[1]).unwrap();
assert_eq!(String::from_utf8(decoded).unwrap(), "line one\nline two");
}
#[test]
fn test_protect_fence_block_plugin_unclosed_left_untouched() {
let input = ":::alert warning\nSomething went wrong";
let output = protect_fence_block_plugins(input);
assert_eq!(output, input);
}
#[test]
fn test_protect_fence_block_plugin_nested_not_supported() {
let input = ":::outer args\n:::inner args2\ncontent\n:::\n:::";
let output = protect_fence_block_plugins(input);
assert!(output.contains("FENCE_BLOCK_PLUGIN:outer:args:"));
assert!(output.trim_end().ends_with(":::"));
}
#[test]
fn test_restore_clear_from_fence_block() {
let protected = protect_fence_block_plugins(":::clear\n:::");
let output = restore_markers(&protected);
assert_eq!(output, "<div class=\"clearfix\"></div>");
}
}