use std::collections::HashMap;
use regex::Regex;
pub trait MarkdownPlugin {
fn name(&self) -> &str;
fn description(&self) -> &str {
""
}
fn render(&self, content: &str) -> Option<String>;
fn can_handle(&self, _syntax: &str) -> bool {
false
}
fn version(&self) -> &str {
"0.1.0"
}
}
pub struct MarkdownRenderer {
plugin_manager: PluginManager,
custom_styles: HashMap<String, String>,
}
impl MarkdownRenderer {
pub fn new() -> Self {
Self {
plugin_manager: PluginManager::new(),
custom_styles: HashMap::new(),
}
}
pub fn register_plugin(&mut self, plugin: Box<dyn MarkdownPlugin>) {
self.plugin_manager.register(plugin);
}
pub fn add_style(&mut self, class: &str, style: &str) {
self.custom_styles
.insert(class.to_string(), style.to_string());
}
pub fn render(&self, input: &str) -> String {
let mut output = String::new();
let mut remaining = input;
for plugin in &self.plugin_manager.plugins {
if let Some(result) = plugin.render(remaining) {
return result;
}
}
for line in input.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
output.push_str("<br>");
continue;
}
if let Some(level) = trimmed.strip_prefix('#').map(|s| s.len() as u8) {
if level <= 6 {
let content = trimmed.trim_start_matches('#').trim();
output.push_str(&format!(
"<h{level}>{}</h{level}>\n",
self.render_inline(content)
));
continue;
}
}
if trimmed.starts_with("---")
|| trimmed.starts_with("***")
|| trimmed.starts_with("___")
{
output.push_str("<hr>\n");
continue;
}
if trimmed.starts_with("```") {
let lang = trimmed.strip_prefix("```").unwrap_or("").trim();
output.push_str(&format!("<pre><code class=\"language-{lang}\">"));
continue;
}
if let Some(content) = trimmed.strip_prefix('>') {
output.push_str(&format!(
"<blockquote>{}</blockquote>\n",
self.render_inline(content.trim())
));
continue;
}
output.push_str(&format!("<p>{}</p>\n", self.render_inline(trimmed)));
}
output
}
fn render_inline(&self, input: &str) -> String {
let mut output = input.to_string();
output = regex_replace(&output, r"\*\*(.+?)\*\*", "<strong>$1</strong>");
output = regex_replace(&output, r"__(.+?)__", "<strong>$1</strong>");
output = regex_replace(&output, r"\*(.+?)\*", "<em>$1</em>");
output = regex_replace(&output, r"_(.+?)_", "<em>$1</em>");
output = regex_replace(&output, r"`(.+?)`", "<code>$1</code>");
output = regex_replace(&output, r"\[(.+?)\]\((.+?)\)", "<a href=\"$2\">$1</a>");
output = regex_replace(
&output,
r"!\[(.+?)\]\((.+?)\)",
"<img src=\"$2\" alt=\"$1\">",
);
output
}
}
impl Default for MarkdownRenderer {
fn default() -> Self {
Self::new()
}
}
pub struct PluginManager {
plugins: Vec<Box<dyn MarkdownPlugin>>,
}
impl PluginManager {
pub fn new() -> Self {
Self {
plugins: Vec::new(),
}
}
pub fn register(&mut self, plugin: Box<dyn MarkdownPlugin>) {
self.plugins.push(plugin);
}
pub fn plugins(&self) -> &[Box<dyn MarkdownPlugin>] {
&self.plugins
}
pub fn find_plugin(&self, name: &str) -> Option<&dyn MarkdownPlugin> {
self.plugins
.iter()
.find(|p| p.name() == name)
.map(|p| p.as_ref())
}
pub fn remove(&mut self, name: &str) -> Option<Box<dyn MarkdownPlugin>> {
if let Some(pos) = self.plugins.iter().position(|p| p.name() == name) {
Some(self.plugins.remove(pos))
} else {
None
}
}
}
impl Default for PluginManager {
fn default() -> Self {
Self::new()
}
}
fn regex_replace(input: &str, pattern: &str, replacement: &str) -> String {
match Regex::new(pattern) {
Ok(re) => re.replace_all(input, replacement).to_string(),
Err(_) => input.to_string(),
}
}
pub struct CodeHighlightPlugin;
impl MarkdownPlugin for CodeHighlightPlugin {
fn name(&self) -> &str {
"code-highlight"
}
fn render(&self, content: &str) -> Option<String> {
if content.starts_with("```") {
let lang = content.trim_start_matches('`').trim();
Some(format!(
"<pre><code class=\"language-{lang}\">CODE</code></pre>"
))
} else {
None
}
}
}
pub struct MathPlugin;
impl MarkdownPlugin for MathPlugin {
fn name(&self) -> &str {
"math"
}
fn can_handle(&self, syntax: &str) -> bool {
syntax.starts_with("$$") || syntax.starts_with("$")
}
fn render(&self, content: &str) -> Option<String> {
if content.starts_with("$$") {
let math = content.trim_start_matches('$').trim();
Some(format!("<div class=\"math-block\">{}</div>", math))
} else if content.starts_with('$') {
let math = content.trim_start_matches('$').trim();
Some(format!("<span class=\"math-inline\">{}</span>", math))
} else {
None
}
}
}
pub struct TaskListPlugin;
impl MarkdownPlugin for TaskListPlugin {
fn name(&self) -> &str {
"task-list"
}
fn can_handle(&self, syntax: &str) -> bool {
syntax.contains("[ ]") || syntax.contains("[x]")
}
fn render(&self, content: &str) -> Option<String> {
let content = content.replace("[ ]", "<input type=\"checkbox\" disabled>");
let content = content.replace("[x]", "<input type=\"checkbox\" checked disabled>");
Some(content)
}
}