Files
log/src/markdown.rs
2025-06-14 13:17:09 +09:00

154 lines
5.7 KiB
Rust

use anyhow::Result;
use pulldown_cmark::{html, Options, Parser, CodeBlockKind};
use syntect::parsing::SyntaxSet;
use syntect::highlighting::ThemeSet;
use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
use gray_matter::Matter;
use gray_matter::engine::YAML;
use serde_json::Value;
pub struct MarkdownProcessor {
highlight_code: bool,
syntax_set: SyntaxSet,
theme_set: ThemeSet,
}
impl MarkdownProcessor {
pub fn new(highlight_code: bool) -> Self {
Self {
highlight_code,
syntax_set: SyntaxSet::load_defaults_newlines(),
theme_set: ThemeSet::load_defaults(),
}
}
pub fn parse_frontmatter(&self, content: &str) -> Result<(serde_json::Map<String, Value>, String)> {
let matter = Matter::<YAML>::new();
let result = matter.parse(content);
let frontmatter = result.data
.and_then(|pod| pod.as_hashmap().ok())
.map(|map| {
let mut json_map = serde_json::Map::new();
for (k, v) in map {
// Keys in hashmap are already strings
let value = self.pod_to_json_value(v);
json_map.insert(k, value);
}
json_map
})
.unwrap_or_default();
Ok((frontmatter, result.content))
}
fn pod_to_json_value(&self, pod: gray_matter::Pod) -> Value {
match pod {
gray_matter::Pod::Null => Value::Null,
gray_matter::Pod::Boolean(b) => Value::Bool(b),
gray_matter::Pod::Integer(i) => Value::Number(serde_json::Number::from(i)),
gray_matter::Pod::Float(f) => serde_json::Number::from_f64(f)
.map(Value::Number)
.unwrap_or(Value::Null),
gray_matter::Pod::String(s) => Value::String(s),
gray_matter::Pod::Array(arr) => {
Value::Array(arr.into_iter().map(|p| self.pod_to_json_value(p)).collect())
}
gray_matter::Pod::Hash(map) => {
let mut json_map = serde_json::Map::new();
for (k, v) in map {
json_map.insert(k, self.pod_to_json_value(v));
}
Value::Object(json_map)
}
}
}
pub fn render(&self, content: &str) -> Result<String> {
let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_TASKLISTS);
if self.highlight_code {
self.render_with_syntax_highlighting(content, options)
} else {
let parser = Parser::new_ext(content, options);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
Ok(html_output)
}
}
fn render_with_syntax_highlighting(&self, content: &str, options: Options) -> Result<String> {
let parser = Parser::new_ext(content, options);
let mut html_output = String::new();
let mut code_block = None;
let theme = &self.theme_set.themes["base16-ocean.dark"];
let mut events = Vec::new();
for event in parser {
match event {
pulldown_cmark::Event::Start(pulldown_cmark::Tag::CodeBlock(kind)) => {
if let CodeBlockKind::Fenced(lang_info) = &kind {
code_block = Some((String::new(), lang_info.to_string()));
}
}
pulldown_cmark::Event::Text(text) => {
if let Some((ref mut code, _)) = code_block {
code.push_str(&text);
} else {
events.push(pulldown_cmark::Event::Text(text));
}
}
pulldown_cmark::Event::End(pulldown_cmark::TagEnd::CodeBlock) => {
if let Some((code, lang_info)) = code_block.take() {
let highlighted = self.highlight_code_block(&code, &lang_info, theme);
events.push(pulldown_cmark::Event::Html(highlighted.into()));
}
}
_ => events.push(event),
}
}
html::push_html(&mut html_output, events.into_iter());
Ok(html_output)
}
fn highlight_code_block(&self, code: &str, lang_info: &str, theme: &syntect::highlighting::Theme) -> String {
// Parse language and filename from lang_info (e.g., "sh:/path/to/file" or "rust:main.rs")
let (lang, filename) = if lang_info.contains(':') {
let parts: Vec<&str> = lang_info.splitn(2, ':').collect();
(parts[0], Some(parts[1]))
} else {
(lang_info, None)
};
let syntax = self.syntax_set
.find_syntax_by_token(lang)
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme);
// Create pre tag with optional filename attribute
let pre_tag = if let Some(filename) = filename {
format!("<pre data-filename=\"{}\">", filename)
} else {
"<pre>".to_string()
};
let mut output = format!("{}<code>", pre_tag);
for line in code.lines() {
let ranges = highlighter.highlight_line(line, &self.syntax_set).unwrap();
let html_line = styled_line_to_highlighted_html(&ranges[..], IncludeBackground::No).unwrap();
output.push_str(&html_line);
output.push('\n');
}
output.push_str("</code></pre>");
output
}
}