Skip to content

Commit

Permalink
Esnure that the frontmatter is never included when rendering post con…
Browse files Browse the repository at this point in the history
…tents
  • Loading branch information
tjk committed Nov 26, 2024
1 parent 2470c19 commit 94c8ea1
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 22 deletions.
12 changes: 11 additions & 1 deletion src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ impl MarkdownProcessor {
}

pub fn render(&self, content: &str) -> String {
let parser = MarkdownParser::new_ext(content, self.options);
let content_without_frontmatter = if let Some(stripped) = content.strip_prefix("---") {
if let Some(end_idx) = stripped.find("---") {
stripped[end_idx + 3..].trim_start()
} else {
content
}
} else {
content
};

let parser = MarkdownParser::new_ext(content_without_frontmatter, self.options);
let mut events = Vec::new();
let mut code_buffer = String::new();
let mut in_code_block = false;
Expand Down
89 changes: 78 additions & 11 deletions src/post.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use crate::config::Config;
use crate::errors::Error;
use crate::markdown::MarkdownProcessor;
use chrono::{Local, NaiveDate};
use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use yaml_front_matter::Document;
use yaml_front_matter::YamlFrontMatter;
use yaml_front_matter::{Document, YamlFrontMatter};

use crate::config::Config;
use crate::errors::Error;
use crate::markdown::MarkdownProcessor;

#[derive(Debug, Serialize)]
pub struct Post {
Expand All @@ -26,11 +27,14 @@ impl Post {
message: e.to_string(),
})?;

Ok(Self {
let mut post = Self {
metadata: doc.metadata,
content: doc.content.clone(),
html_content: md_proc.render(&content),
})
html_content: md_proc.render(&doc.content),
};

post.metadata.read_time = calculate_read_time(&doc.content);
Ok(post)
}
// Get the assets directory for this post
pub fn assets_dir(&self, config: &Config) -> PathBuf {
Expand Down Expand Up @@ -88,6 +92,8 @@ pub struct PostMetadata {
pub preview: String,
#[serde(deserialize_with = "validate_and_slugify")]
pub slug: String,
#[serde(default)]
pub read_time: u32,
}

fn default_author() -> String {
Expand Down Expand Up @@ -116,6 +122,42 @@ where
Ok(slug)
}

// Calculate read time in minutes based on word count
fn calculate_read_time(content: &str) -> u32 {
const WORDS_PER_MINUTE: u32 = 200; // Average adult reading speed

// Create a markdown parser with basic options
let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TABLES);

let parser = Parser::new_ext(content, options);

// Count words only in actual content, excluding code blocks and YAML frontmatter
let mut word_count = 0;
let mut in_code_block = false;

for event in parser {
match event {
Event::Start(Tag::CodeBlock(_)) => in_code_block = true,
Event::End(TagEnd::CodeBlock) => in_code_block = false,
Event::Text(text) if !in_code_block => {
// Count words in text content
word_count += text.split_whitespace().count();
}
_ => {}
}
}

// Calculate minutes rounded up to the nearest minute
let minutes = (word_count as f32 / WORDS_PER_MINUTE as f32).ceil() as u32;
if minutes == 0 {
1
} else {
minutes
}
}

pub fn slugify(text: &str) -> String {
let slug = text
.to_lowercase()
Expand Down Expand Up @@ -180,10 +222,10 @@ slug: "{}"
{}
{}"#,
title,
title.trim(),
date,
slug,
outline.as_ref().unwrap_or(&String::new()),
slug.trim(),
outline.as_ref().map(|o| o.trim()).unwrap_or(""),
if outline.is_some() {
"<!-- Generated outline above. Replace with your content. -->"
} else {
Expand Down Expand Up @@ -365,3 +407,28 @@ slug: "test-post"
assert_eq!(doc.metadata.preview, "");
}
}

#[test]
fn test_read_time_in_post_metadata() {
let content = r#"---
title: "Test Post"
date: "2024-01-01"
slug: "test-post"
---
This is a test post with enough words to make it take at least one minute to read.
Let's add some more text to make sure we have enough content to test the read time calculation.
We'll keep adding words until we have enough for a proper test of the functionality."#;

let temp_dir = tempfile::TempDir::new().unwrap();
let test_file = temp_dir.path().join("test-post.md");
fs::write(&test_file, content).unwrap();

let md_proc = MarkdownProcessor::new();
let post = Post::new_from_path(&test_file, &md_proc).unwrap();

assert!(
post.metadata.read_time > 0,
"Read time should be calculated and greater than 0"
);
}
23 changes: 13 additions & 10 deletions src/serve.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crate::generator::SiteGenerator;
use actix_files::Files;
use actix_web::middleware::Logger;
use actix_web::{web, App, HttpResponse, HttpServer};
use notify::{RecursiveMode, Watcher};

use std::process::Command;
use std::sync::mpsc;
use std::time::{Duration, Instant};

use crate::config::Config;
use crate::errors::Error;

pub struct Server {
config: Config,
Expand Down Expand Up @@ -47,6 +48,7 @@ impl Server {
}
}

let config_for_rebuild = self.config.clone();
let _watcher_handler = std::thread::spawn(move || {
let mut last_build = Instant::now();
let debounce_duration = Duration::from_millis(500);
Expand All @@ -57,18 +59,14 @@ impl Server {

if last_build.elapsed() >= debounce_duration {
println!("🔄 Rebuilding site...");
match Command::new("termv")
.arg("build")
.arg("--target-dir")
.arg(&config_clone.site_dir)
.status()
{
Ok(status) if status.success() => {

// Use internal generator instead of spawning process
match rebuild_site(&config_for_rebuild) {
Ok(_) => {
println!("✨ Site rebuilt successfully!");
last_build = Instant::now();
}
Ok(status) => eprintln!("Build failed with status: {}", status),
Err(e) => eprintln!("Failed to execute build: {}", e),
Err(e) => eprintln!("Build failed: {}", e),
}
}
}
Expand Down Expand Up @@ -97,6 +95,11 @@ impl Server {
}
}

fn rebuild_site(config: &Config) -> Result<(), Error> {
let generator = SiteGenerator::new(config)?;
generator.generate_site()
}

pub fn serve(config: Config) -> Result<(), Box<dyn std::error::Error>> {
let output_dir = config.output_dir();

Expand Down

0 comments on commit 94c8ea1

Please sign in to comment.