use std::fs;
use std::thread::sleep;
use std::time::Duration;
use mini_docs::{Builder, DocError, MarkdownAnalyzer};
use serde_json::{json, Value};
const MTIME_SETTLE: Duration = Duration::from_millis(50);
struct WordCountAnalyzer;
impl MarkdownAnalyzer for WordCountAnalyzer {
fn analyze(&self, body: &str, _frontmatter: &Value) -> Result<Value, DocError> {
let word_count = body.split_whitespace().count();
Ok(json!({
"word_count": word_count
}))
}
fn name(&self) -> &str {
"word_count"
}
}
struct ReadingTimeAnalyzer;
impl MarkdownAnalyzer for ReadingTimeAnalyzer {
fn analyze(&self, body: &str, _frontmatter: &Value) -> Result<Value, DocError> {
let word_count = body.split_whitespace().count();
let minutes = std::cmp::max(1, word_count / 200); Ok(json!({
"estimated_minutes": minutes
}))
}
fn name(&self) -> &str {
"reading_time"
}
}
struct ConflictAnalyzer;
impl MarkdownAnalyzer for ConflictAnalyzer {
fn analyze(&self, _body: &str, _frontmatter: &Value) -> Result<Value, DocError> {
Ok(json!({}))
}
fn name(&self) -> &str {
"word_count" }
}
#[test]
fn analyzer_output_accessible_in_template() {
let input = tempfile::tempdir().expect("create input tempdir");
let templates = tempfile::tempdir().expect("create templates tempdir");
let output = tempfile::tempdir().expect("create output tempdir");
fs::write(
input.path().join("page.md"),
"# Test\n\nOne two three four five.\n",
)
.expect("write page.md");
fs::write(
templates.path().join("page.html"),
"Word count: {{ page.extensions.word_count.word_count }}",
)
.expect("write page.html");
Builder::new(input.path())
.templates(templates.path())
.output(output.path())
.default_template("page.html")
.analyzer(WordCountAnalyzer)
.build()
.expect("build should succeed");
let written = fs::read_to_string(output.path().join("page.html")).expect("read output file");
assert!(
written.contains("Word count: 7"),
"analyzer output should be rendered in template: {written}"
);
}
#[test]
fn duplicate_analyzer_names_cause_error() {
let input = tempfile::tempdir().expect("create input tempdir");
let templates = tempfile::tempdir().expect("create templates tempdir");
let output = tempfile::tempdir().expect("create output tempdir");
fs::write(input.path().join("page.md"), "# Test\n\nBody.\n").expect("write page.md");
fs::write(
templates.path().join("page.html"),
"{{ page.content | safe }}",
)
.expect("write page.html");
let result = Builder::new(input.path())
.templates(templates.path())
.output(output.path())
.default_template("page.html")
.analyzer(WordCountAnalyzer)
.analyzer(ConflictAnalyzer)
.build();
match result {
Err(DocError::Extension(msg)) => {
assert!(
msg.contains("word_count"),
"error message should mention the conflicting name: {msg}"
);
}
_ => panic!("expected DocError::Extension with duplicate name, got {result:?}"),
}
assert!(
!output.path().join("page.html").exists(),
"no output should be written when build fails"
);
}
#[test]
fn analyzer_works_in_watch_path() {
let input = tempfile::tempdir().expect("create input tempdir");
let templates = tempfile::tempdir().expect("create templates tempdir");
let output = tempfile::tempdir().expect("create output tempdir");
fs::write(
input.path().join("page.md"),
"# Test\n\nOne two three.\n",
)
.expect("write page.md");
fs::write(
templates.path().join("page.html"),
"Count: {{ page.extensions.word_count.word_count }}",
)
.expect("write page.html");
let builder = Builder::new(input.path())
.templates(templates.path())
.output(output.path())
.default_template("page.html")
.analyzer(WordCountAnalyzer);
let mut watcher = builder.watch().expect("initial watch build should succeed");
let initial = fs::read_to_string(output.path().join("page.html")).expect("read output file");
assert!(
initial.contains("Count: 5"),
"analyzer should run during initial watch build: {initial}"
);
sleep(MTIME_SETTLE);
fs::write(
input.path().join("page.md"),
"# Test\n\nOne two three four five.\n",
)
.expect("rewrite page.md");
let changed = watcher.tick().expect("tick should succeed");
assert_eq!(
changed.len(),
1,
"the page should be rebuilt on tick: {changed:?}"
);
let after = fs::read_to_string(output.path().join("page.html")).expect("read updated output");
assert!(
after.contains("Count: 7"),
"analyzer should run on watch tick with updated word count: {after}"
);
}
#[test]
fn multiple_analyzers_results_merged() {
let input = tempfile::tempdir().expect("create input tempdir");
let templates = tempfile::tempdir().expect("create templates tempdir");
let output = tempfile::tempdir().expect("create output tempdir");
let body = (0..100).map(|i| format!("word{}", i)).collect::<Vec<_>>().join(" ");
fs::write(input.path().join("page.md"), &format!("# Test\n\n{body}\n"))
.expect("write page.md");
fs::write(
templates.path().join("page.html"),
"Words: {{ page.extensions.word_count.word_count }}, Time: {{ page.extensions.reading_time.estimated_minutes }}m",
)
.expect("write page.html");
Builder::new(input.path())
.templates(templates.path())
.output(output.path())
.default_template("page.html")
.analyzer(WordCountAnalyzer)
.analyzer(ReadingTimeAnalyzer)
.build()
.expect("build should succeed");
let written = fs::read_to_string(output.path().join("page.html")).expect("read output file");
assert!(
written.contains("Words: 102"),
"word count analyzer should be in context: {written}"
);
assert!(
written.contains("Time:"),
"reading time analyzer should be in context: {written}"
);
}
#[test]
fn analyzers_receive_processed_body_from_processors() {
use mini_docs::MarkdownProcessor;
let input = tempfile::tempdir().expect("create input tempdir");
let templates = tempfile::tempdir().expect("create templates tempdir");
let output = tempfile::tempdir().expect("create output tempdir");
fs::write(input.path().join("page.md"), "# Test\n\nOriginal body.\n")
.expect("write page.md");
fs::write(
templates.path().join("page.html"),
"Count: {{ page.extensions.word_count.word_count }}",
)
.expect("write page.html");
struct AppendProcessor;
impl MarkdownProcessor for AppendProcessor {
fn process(&self, body: &str, _frontmatter: &Value) -> Result<String, DocError> {
Ok(format!("{body}\n\nAppended content here."))
}
fn name(&self) -> &str {
"appender"
}
}
Builder::new(input.path())
.templates(templates.path())
.output(output.path())
.default_template("page.html")
.processor(AppendProcessor)
.analyzer(WordCountAnalyzer)
.build()
.expect("build should succeed");
let written = fs::read_to_string(output.path().join("page.html")).expect("read output file");
assert!(
written.contains("Count: 7"),
"analyzer should see the processed body (including appended content): {written}"
);
}