170 lines
5.3 KiB
Rust
170 lines
5.3 KiB
Rust
|
|
//! Diary subsystem (Phase 16) — pure helpers used by the
|
||
|
|
//! `nuwiki.diary.*` `executeCommand` handlers.
|
||
|
|
//!
|
||
|
|
//! All path/URI math lives here so the command dispatcher stays a thin
|
||
|
|
//! wrapper. The diary navigation model is intentionally lightweight:
|
||
|
|
//! entries are identified by file stem (`YYYY-MM-DD`) living under
|
||
|
|
//! `<wiki_root>/<diary_rel_path>/`. The `WorkspaceIndex` tags each
|
||
|
|
//! `IndexedPage` with a `diary_date` so prev/next/list operations stay
|
||
|
|
//! O(n) over the indexed pages.
|
||
|
|
|
||
|
|
use std::path::Path;
|
||
|
|
|
||
|
|
use tower_lsp::lsp_types::Url;
|
||
|
|
|
||
|
|
use nuwiki_core::date::DiaryDate;
|
||
|
|
|
||
|
|
use crate::config::WikiConfig;
|
||
|
|
use crate::index::WorkspaceIndex;
|
||
|
|
|
||
|
|
/// Resolve a `DiaryDate` to its `file://` URI under the given wiki.
|
||
|
|
pub fn uri_for_date(cfg: &WikiConfig, date: &DiaryDate) -> Option<Url> {
|
||
|
|
Url::from_file_path(cfg.diary_path_for(date)).ok()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// `file://` URI of the diary index page.
|
||
|
|
pub fn index_uri(cfg: &WikiConfig) -> Option<Url> {
|
||
|
|
Url::from_file_path(cfg.diary_index_path()).ok()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// All diary entries currently indexed for `wiki`, sorted ascending by
|
||
|
|
/// date. The wiki's index must already have its `diary_rel_path` set
|
||
|
|
/// (handled by `Wiki::new`).
|
||
|
|
pub fn list_entries(index: &WorkspaceIndex) -> Vec<DiaryEntry> {
|
||
|
|
let mut out: Vec<DiaryEntry> = index
|
||
|
|
.pages_by_uri
|
||
|
|
.values()
|
||
|
|
.filter_map(|p| {
|
||
|
|
p.diary_date.map(|d| DiaryEntry {
|
||
|
|
date: d,
|
||
|
|
uri: p.uri.clone(),
|
||
|
|
})
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
out.sort_by(|a, b| a.date.cmp(&b.date));
|
||
|
|
out
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Entries for a given (year, month). `month = None` returns the whole
|
||
|
|
/// year; `year = None` matches every year.
|
||
|
|
pub fn list_entries_filtered(
|
||
|
|
index: &WorkspaceIndex,
|
||
|
|
year: Option<i32>,
|
||
|
|
month: Option<u8>,
|
||
|
|
) -> Vec<DiaryEntry> {
|
||
|
|
list_entries(index)
|
||
|
|
.into_iter()
|
||
|
|
.filter(|e| year.is_none_or(|y| e.date.year == y))
|
||
|
|
.filter(|e| month.is_none_or(|m| e.date.month == m))
|
||
|
|
.collect()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The diary entry chronologically after `from`. `from` doesn't need to
|
||
|
|
/// be indexed itself — we look for the smallest indexed date strictly
|
||
|
|
/// greater than `from`.
|
||
|
|
pub fn next_entry(index: &WorkspaceIndex, from: &DiaryDate) -> Option<DiaryEntry> {
|
||
|
|
list_entries(index).into_iter().find(|e| e.date > *from)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The diary entry chronologically before `from`.
|
||
|
|
pub fn prev_entry(index: &WorkspaceIndex, from: &DiaryDate) -> Option<DiaryEntry> {
|
||
|
|
list_entries(index)
|
||
|
|
.into_iter()
|
||
|
|
.rev()
|
||
|
|
.find(|e| e.date < *from)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Generate the body of the diary index page — a flat newest-first list
|
||
|
|
/// of `[[diary/YYYY-MM-DD]]` wikilinks, grouped under year and month
|
||
|
|
/// subheadings so the rendered page mirrors `:VimwikiDiaryGenerateLinks`.
|
||
|
|
pub fn build_index_body(
|
||
|
|
entries: &[DiaryEntry],
|
||
|
|
diary_rel_path: &str,
|
||
|
|
index_heading: &str,
|
||
|
|
) -> String {
|
||
|
|
let mut out = String::new();
|
||
|
|
out.push_str("= ");
|
||
|
|
out.push_str(index_heading);
|
||
|
|
out.push_str(" =\n");
|
||
|
|
|
||
|
|
if entries.is_empty() {
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sort descending by date — newest first matches vimwiki.
|
||
|
|
let mut sorted: Vec<&DiaryEntry> = entries.iter().collect();
|
||
|
|
sorted.sort_by(|a, b| b.date.cmp(&a.date));
|
||
|
|
|
||
|
|
let mut current_year: Option<i32> = None;
|
||
|
|
let mut current_month: Option<u8> = None;
|
||
|
|
for e in sorted {
|
||
|
|
if current_year != Some(e.date.year) {
|
||
|
|
out.push_str(&format!("\n== {} ==\n", e.date.year));
|
||
|
|
current_year = Some(e.date.year);
|
||
|
|
current_month = None;
|
||
|
|
}
|
||
|
|
if current_month != Some(e.date.month) {
|
||
|
|
out.push_str(&format!("=== {} ===\n", month_name(e.date.month)));
|
||
|
|
current_month = Some(e.date.month);
|
||
|
|
}
|
||
|
|
out.push_str(&format!(
|
||
|
|
"- [[{}/{}]]\n",
|
||
|
|
diary_rel_path.trim_end_matches('/'),
|
||
|
|
e.date.format()
|
||
|
|
));
|
||
|
|
}
|
||
|
|
out
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Render the diary-index body for the given wiki's currently-indexed
|
||
|
|
/// entries. Convenience wrapper around [`build_index_body`].
|
||
|
|
pub fn render_index_body(cfg: &WikiConfig, index: &WorkspaceIndex) -> String {
|
||
|
|
build_index_body(&list_entries(index), &cfg.diary_rel_path, "Diary")
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Cheap "is this path under the diary subdir of this wiki" check —
|
||
|
|
/// surface-level, no parse needed.
|
||
|
|
pub fn is_in_diary(cfg: &WikiConfig, path: &Path) -> bool {
|
||
|
|
path.starts_with(cfg.diary_dir())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// One diary entry, returned by listing/navigation queries. The custom
|
||
|
|
/// `Serialize` emits `{ "date": "YYYY-MM-DD", "uri": "file://..." }` so
|
||
|
|
/// the client doesn't have to know about [`DiaryDate`]'s internal layout.
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
|
|
pub struct DiaryEntry {
|
||
|
|
pub date: DiaryDate,
|
||
|
|
pub uri: Url,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl serde::Serialize for DiaryEntry {
|
||
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||
|
|
where
|
||
|
|
S: serde::Serializer,
|
||
|
|
{
|
||
|
|
use serde::ser::SerializeStruct;
|
||
|
|
let mut s = serializer.serialize_struct("DiaryEntry", 2)?;
|
||
|
|
s.serialize_field("date", &self.date.format())?;
|
||
|
|
s.serialize_field("uri", &self.uri)?;
|
||
|
|
s.end()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn month_name(m: u8) -> &'static str {
|
||
|
|
match m {
|
||
|
|
1 => "January",
|
||
|
|
2 => "February",
|
||
|
|
3 => "March",
|
||
|
|
4 => "April",
|
||
|
|
5 => "May",
|
||
|
|
6 => "June",
|
||
|
|
7 => "July",
|
||
|
|
8 => "August",
|
||
|
|
9 => "September",
|
||
|
|
10 => "October",
|
||
|
|
11 => "November",
|
||
|
|
12 => "December",
|
||
|
|
_ => "Unknown",
|
||
|
|
}
|
||
|
|
}
|