rust parser working on local build. fix docker build
This commit is contained in:
181
markdown_backend/src/markdown.rs
Normal file
181
markdown_backend/src/markdown.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
// src/markdown.rs
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Deserialize;
|
||||
use pulldown_cmark::{Parser, Options, html};
|
||||
use gray_matter::engine::YAML;
|
||||
use gray_matter::Matter;
|
||||
use ammonia::clean;
|
||||
use slug::slugify;
|
||||
use notify::{RecursiveMode, RecommendedWatcher, Watcher, Config};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Duration;
|
||||
use syntect::highlighting::{ThemeSet, Style};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use syntect::html::{highlighted_html_for_string, IncludeBackground};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, serde::Serialize)]
|
||||
pub struct PostFrontmatter {
|
||||
pub title: String,
|
||||
pub date: String,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct Post {
|
||||
pub slug: String,
|
||||
pub title: String,
|
||||
pub date: String,
|
||||
pub tags: Vec<String>,
|
||||
pub summary: Option<String>,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub author: String,
|
||||
}
|
||||
|
||||
fn get_posts_directory() -> PathBuf {
|
||||
let candidates = [
|
||||
"./posts",
|
||||
"../posts",
|
||||
"/posts",
|
||||
"/docker"
|
||||
];
|
||||
for candidate in candidates.iter() {
|
||||
let path = PathBuf::from(candidate);
|
||||
if path.exists() && path.is_dir() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
// Fallback: default to ./posts
|
||||
PathBuf::from("./posts")
|
||||
}
|
||||
|
||||
fn get_file_creation_date(path: &Path) -> std::io::Result<DateTime<Utc>> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
let created = metadata.created()?;
|
||||
Ok(DateTime::<Utc>::from(created))
|
||||
}
|
||||
|
||||
fn process_anchor_links(content: &str) -> String {
|
||||
// Replace [text](#anchor) with slugified anchor
|
||||
let re = regex::Regex::new(r"\[([^\]]+)\]\(#([^)]+)\)").unwrap();
|
||||
re.replace_all(content, |caps: ®ex::Captures| {
|
||||
let link_text = &caps[1];
|
||||
let anchor = &caps[2];
|
||||
let slugified = slugify(anchor);
|
||||
format!("[{}](#{})", link_text, slugified)
|
||||
}).to_string()
|
||||
}
|
||||
|
||||
fn highlight_code_blocks(html: &str) -> String {
|
||||
let ss = SyntaxSet::load_defaults_newlines();
|
||||
let ts = ThemeSet::load_defaults();
|
||||
let theme = &ts.themes["base16-ocean.dark"];
|
||||
|
||||
// Simple code block detection and highlighting
|
||||
// In a real implementation, you'd want to parse the HTML and handle code blocks properly
|
||||
let re = regex::Regex::new(r#"<pre><code class="language-([^"]+)">([^<]+)</code></pre>"#).unwrap();
|
||||
re.replace_all(html, |caps: ®ex::Captures| {
|
||||
let lang = &caps[1];
|
||||
let code = &caps[2];
|
||||
|
||||
if let Some(syntax) = ss.find_syntax_by_token(lang) {
|
||||
match highlighted_html_for_string(code, &ss, syntax, theme) {
|
||||
Ok(highlighted) => highlighted,
|
||||
Err(_) => caps[0].to_string(),
|
||||
}
|
||||
} else {
|
||||
caps[0].to_string()
|
||||
}
|
||||
}).to_string()
|
||||
}
|
||||
|
||||
pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>> {
|
||||
let posts_dir = get_posts_directory();
|
||||
let file_path = posts_dir.join(format!("{}.md", slug));
|
||||
let file_content = fs::read_to_string(&file_path)?;
|
||||
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(&file_content);
|
||||
|
||||
let front: PostFrontmatter = if let Some(data) = result.data {
|
||||
match data.deserialize() {
|
||||
Ok(front) => front,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to deserialize frontmatter for post {}: {}", slug, e);
|
||||
return Err("Failed to deserialize frontmatter".into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("No frontmatter found for post: {}", slug);
|
||||
return Err("No frontmatter found".into());
|
||||
};
|
||||
|
||||
let created_at = get_file_creation_date(&file_path)?;
|
||||
|
||||
let processed_markdown = process_anchor_links(&result.content);
|
||||
let mut html_output = String::new();
|
||||
let parser = Parser::new_ext(&processed_markdown, Options::all());
|
||||
html::push_html(&mut html_output, parser);
|
||||
|
||||
let highlighted_html = highlight_code_blocks(&html_output);
|
||||
let sanitized_html = clean(&highlighted_html);
|
||||
|
||||
Ok(Post {
|
||||
slug: slug.to_string(),
|
||||
title: front.title,
|
||||
date: front.date,
|
||||
tags: front.tags.unwrap_or_default(),
|
||||
summary: front.summary,
|
||||
content: sanitized_html,
|
||||
created_at: created_at.to_rfc3339(),
|
||||
author: std::env::var("BLOG_OWNER").unwrap_or_else(|_| "Anonymous".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_all_posts() -> Result<Vec<Post>, Box<dyn std::error::Error>> {
|
||||
let posts_dir = get_posts_directory();
|
||||
let mut posts = Vec::new();
|
||||
for entry in fs::read_dir(posts_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.extension().map(|e| e == "md").unwrap_or(false) {
|
||||
let file_stem = path.file_stem().unwrap().to_string_lossy();
|
||||
if let Ok(post) = get_post_by_slug(&file_stem) {
|
||||
posts.push(post);
|
||||
}
|
||||
}
|
||||
}
|
||||
posts.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
Ok(posts)
|
||||
}
|
||||
|
||||
pub fn get_posts_by_tag(tag: &str) -> Result<Vec<Post>, Box<dyn std::error::Error>> {
|
||||
let all_posts = get_all_posts()?;
|
||||
Ok(all_posts.into_iter().filter(|p| p.tags.contains(&tag.to_string())).collect())
|
||||
}
|
||||
|
||||
pub fn watch_posts<F: Fn() + Send + 'static>(on_change: F) -> notify::Result<RecommendedWatcher> {
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
|
||||
watcher.watch(get_posts_directory().as_path(), RecursiveMode::Recursive)?;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(_event) => {
|
||||
on_change();
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("watch error: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(watcher)
|
||||
}
|
||||
Reference in New Issue
Block a user