use doing_config::Config;
use doing_taskpaper::Entry;
use doing_template::renderer::RenderOptions;
use crate::{ExportPlugin, Plugin, PluginSettings, helpers};
const MARKDOWN_DATE_FORMAT: &str = "%a %-I:%M%p";
pub struct MarkdownExport;
impl ExportPlugin for MarkdownExport {
fn render(&self, entries: &[Entry], _options: &RenderOptions, config: &Config) -> String {
let sections = helpers::group_by_section(entries);
let mut out = String::new();
for (section, items) in §ions {
if !out.is_empty() {
out.push('\n');
}
out.push_str(&format!("## {section}\n\n"));
for entry in items {
let done = if entry.finished() { "x" } else { " " };
let date = entry.date().format(MARKDOWN_DATE_FORMAT).to_string();
let title = entry.full_title();
let time_str = helpers::format_interval(entry, config)
.map(|t| format!(" [**{t}**]"))
.unwrap_or_default();
out.push_str(&format!("- [{done}] {date} {title}{time_str}"));
if !entry.note().is_empty() {
out.push_str("\n\n");
for line in entry.note().lines() {
out.push_str(&format!(" {}\n", line.trim()));
}
}
out.push('\n');
}
}
out
}
}
impl Plugin for MarkdownExport {
fn name(&self) -> &str {
"markdown"
}
fn settings(&self) -> PluginSettings {
PluginSettings {
trigger: "markdown|mk?d|gfm".into(),
}
}
}
#[cfg(test)]
mod test {
use doing_taskpaper::{Note, Tag, Tags};
use super::*;
use crate::test_helpers::{sample_date, sample_options};
mod markdown_export_name {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_returns_markdown() {
assert_eq!(MarkdownExport.name(), "markdown");
}
}
mod markdown_export_render {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_renders_empty_entries() {
let config = Config::default();
let options = sample_options();
let output = MarkdownExport.render(&[], &options, &config);
assert_eq!(output, "");
}
#[test]
fn it_does_not_include_top_level_heading() {
let config = Config::default();
let options = sample_options();
let entry = Entry::new(
sample_date(17, 14, 30),
"Task",
Tags::new(),
Note::new(),
"Currently",
None::<String>,
);
let output = MarkdownExport.render(&[entry], &options, &config);
assert!(!output.contains("# what are you doing?"));
}
#[test]
fn it_renders_finished_entry_with_checked_box() {
let config = Config::default();
let options = sample_options();
let entry = Entry::new(
sample_date(17, 14, 30),
"Completed task",
Tags::from_iter(vec![Tag::new("done", Some("2024-03-17 15:00"))]),
Note::new(),
"Currently",
None::<String>,
);
let output = MarkdownExport.render(&[entry], &options, &config);
assert!(output.contains("- [x]"));
assert!(output.contains("Completed task @done(2024-03-17 15:00)"));
assert!(output.contains("Sun"));
assert!(output.contains("2:30PM"));
}
#[test]
fn it_renders_unfinished_entry_with_unchecked_box() {
let config = Config::default();
let options = sample_options();
let entry = Entry::new(
sample_date(17, 14, 30),
"In progress",
Tags::new(),
Note::new(),
"Currently",
None::<String>,
);
let output = MarkdownExport.render(&[entry], &options, &config);
assert!(output.contains("- [ ]"));
assert!(output.contains("In progress\n"));
}
#[test]
fn it_uses_abbreviated_day_time_date_format() {
let config = Config::default();
let options = sample_options();
let entry = Entry::new(
sample_date(17, 9, 1),
"Morning task",
Tags::new(),
Note::new(),
"Currently",
None::<String>,
);
let output = MarkdownExport.render(&[entry], &options, &config);
assert!(output.contains("Sun 9:01AM"));
}
#[test]
fn it_renders_interval_in_bold() {
let config = Config::default();
let options = sample_options();
let entry = Entry::new(
sample_date(17, 14, 30),
"Working",
Tags::from_iter(vec![
Tag::new("coding", None::<String>),
Tag::new("done", Some("2024-03-17 15:00")),
]),
Note::new(),
"Currently",
None::<String>,
);
let output = MarkdownExport.render(&[entry], &options, &config);
assert!(output.contains("[**00:30:00**]"));
}
#[test]
fn it_renders_notes_indented() {
let config = Config::default();
let options = sample_options();
let entry = Entry::new(
sample_date(17, 14, 30),
"Task",
Tags::new(),
Note::from_text("Note line 1\nNote line 2"),
"Currently",
None::<String>,
);
let output = MarkdownExport.render(&[entry], &options, &config);
assert!(output.contains(" Note line 1\n"));
assert!(output.contains(" Note line 2\n"));
}
#[test]
fn it_renders_section_headers() {
let config = Config::default();
let options = sample_options();
let entries = vec![
Entry::new(
sample_date(17, 14, 0),
"A",
Tags::new(),
Note::new(),
"Currently",
None::<String>,
),
Entry::new(
sample_date(17, 15, 0),
"B",
Tags::new(),
Note::new(),
"Archive",
None::<String>,
),
];
let output = MarkdownExport.render(&entries, &options, &config);
assert!(output.contains("## Currently\n"));
assert!(output.contains("## Archive\n"));
}
#[test]
fn it_renders_tags_inline() {
let config = Config::default();
let options = sample_options();
let entry = Entry::new(
sample_date(17, 14, 30),
"Working on project",
Tags::from_iter(vec![Tag::new("coding", None::<String>)]),
Note::new(),
"Currently",
None::<String>,
);
let output = MarkdownExport.render(&[entry], &options, &config);
assert!(output.contains("Working on project @coding"));
}
}
mod markdown_export_settings {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_returns_markdown_trigger() {
let settings = MarkdownExport.settings();
assert_eq!(settings.trigger, "markdown|mk?d|gfm");
}
}
}