From b8586537f8814eb06e22126302babd7019b07d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fr=C3=B3es=20Franco?= Date: Sun, 31 May 2026 17:06:09 +0000 Subject: [PATCH] feat(diary): restore diary_start_week_day via configurable weekly naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit c63ec67 dropped diary_start_week_day, hardwiring the weekly diary to ISO (Monday) weeks. Upstream vimwiki instead names a weekly note by the week-start day's date (YYYY-MM-DD) and honours diary_start_week_day. Rather than force one scheme, make it a per-wiki choice so migrators keep upstream behaviour while existing nuwiki weekly files keep working: - New per-wiki key `diary_weekly_style`: `iso` (default — `YYYY-Www`, Monday-based, nuwiki's original) or `date`/`vimwiki` (`YYYY-MM-DD` of the week-start day, upstream parity). - Restored per-wiki key `diary_start_week_day` (`monday`..`sunday`, default monday); applies only in `date` mode. Implementation: - nuwiki-core::date gains WeeklyStyle, WeekStart, and DiaryCalendar (owns today/next/prev — date-mode snaps to the week-start and steps ±7 days; iso/daily/monthly/yearly defer to the existing DiaryPeriod logic). - WikiConfig gains the two fields (+ defaults, RawWiki, From) and a diary_calendar() builder; commands.rs diary_open_relative and the next/prev pivot use it. Defaults preserve current behaviour (iso/monday), so no breaking change. Tests: DiaryCalendar cases in nuwiki-core/tests/diary.rs (snap, ±7, sunday start, iso delegation, daily) + config round-trip in nuwiki-lsp/tests/index_and_config.rs. README + doc/nuwiki.txt + lua config comment updated. fmt + clippy clean; all crate tests + config-parity green. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 6 +- crates/nuwiki-core/src/date.rs | 126 ++++++++++++++++++++ crates/nuwiki-core/tests/diary.rs | 96 ++++++++++++++- crates/nuwiki-lsp/src/commands.rs | 9 +- crates/nuwiki-lsp/src/config.rs | 41 +++++++ crates/nuwiki-lsp/tests/index_and_config.rs | 21 ++++ development/vimwiki-gap.md | 19 +-- doc/nuwiki.txt | 13 +- lua/nuwiki/config.lua | 1 + 9 files changed, 318 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f13915a..956ed5e 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,8 @@ require('nuwiki').setup({ diary_rel_path = 'diary', diary_index = 'diary', diary_frequency = 'daily', -- 'daily' | 'weekly' | 'monthly' | 'yearly' + diary_weekly_style = 'iso', -- 'iso' (YYYY-Www) | 'date' (week-start YYYY-MM-DD, vimwiki) + diary_start_week_day = 'monday', -- week start for diary_weekly_style='date' diary_sort = 'desc', -- 'desc' | 'asc' diary_header = 'Diary', -- HTML export @@ -319,7 +321,9 @@ Each is a boolean. All default to `true` except `mouse`. | `diary_rel_path` | string | `'diary'` | path relative to `root` | | `diary_index` | string | `'diary'` | diary index page stem | | `diary_frequency` | string | `'daily'` | `'daily'` \| `'weekly'` \| `'monthly'` \| `'yearly'` | -| `diary_caption_level` | int | `1` | `1`–`6` (level of the index caption) | +| `diary_weekly_style` | string | `'iso'` | `'iso'` (`YYYY-Www`) \| `'date'`/`'vimwiki'` (week-start `YYYY-MM-DD`). Weekly only | +| `diary_start_week_day` | string | `'monday'` | `'monday'`–`'sunday'`; week start when `diary_weekly_style = 'date'` | +| `diary_caption_level` | int | `0` | `0`–`6` (level of the index caption) | | `diary_sort` | string | `'desc'` | `'desc'` \| `'asc'` | | `diary_header` | string | `'Diary'` | any heading text | | `listsyms` | string | `' .oOX'` | checkbox progression chars | diff --git a/crates/nuwiki-core/src/date.rs b/crates/nuwiki-core/src/date.rs index 8687fc3..ef5e29a 100644 --- a/crates/nuwiki-core/src/date.rs +++ b/crates/nuwiki-core/src/date.rs @@ -358,6 +358,132 @@ impl fmt::Display for DiaryPeriod { } } +/// How a *weekly* diary entry is named. +/// +/// `Iso` — ISO-week label `YYYY-Www` (nuwiki's original scheme; the week +/// always begins on Monday, `week_start` is ignored). +/// `Date` — the week-start day's calendar date `YYYY-MM-DD`, matching +/// upstream vimwiki; honours `week_start`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WeeklyStyle { + Iso, + Date, +} + +impl WeeklyStyle { + /// Parse the config string. `date`/`vimwiki` → `Date`; anything else + /// (including `iso`/`week`/empty) → `Iso`. + pub fn parse(s: &str) -> Self { + match s.trim().to_ascii_lowercase().as_str() { + "date" | "vimwiki" | "day" => Self::Date, + _ => Self::Iso, + } + } +} + +/// Day a week begins on, in ISO numbering (Monday = 1 … Sunday = 7). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct WeekStart(u8); + +impl WeekStart { + /// Parse a weekday name (`monday`..`sunday`); unknown → Monday. + pub fn parse(s: &str) -> Self { + let n = match s.trim().to_ascii_lowercase().as_str() { + "tuesday" => 2, + "wednesday" => 3, + "thursday" => 4, + "friday" => 5, + "saturday" => 6, + "sunday" => 7, + _ => 1, // monday (default) + }; + Self(n) + } + + pub fn monday() -> Self { + Self(1) + } + + fn iso(self) -> i64 { + self.0 as i64 + } +} + +/// Diary navigation policy: frequency plus, for weekly diaries, the naming +/// scheme and week-start day. Built from a wiki's `diary_*` config so the +/// LSP commands can compute today / next / prev without re-deriving the +/// rules. `Daily`/`Monthly`/`Yearly` ignore the weekly fields. +#[derive(Debug, Clone, Copy)] +pub struct DiaryCalendar { + pub freq: DiaryFrequency, + pub weekly_style: WeeklyStyle, + pub week_start: WeekStart, +} + +impl DiaryCalendar { + pub fn new(freq: DiaryFrequency, weekly_style: WeeklyStyle, week_start: WeekStart) -> Self { + Self { + freq, + weekly_style, + week_start, + } + } + + fn is_weekly_date(&self) -> bool { + self.freq == DiaryFrequency::Weekly && self.weekly_style == WeeklyStyle::Date + } + + /// "Now" as a diary period for this calendar. Date-mode weekly snaps + /// today back to the most recent week-start day (a plain `Day`, so the + /// file is `YYYY-MM-DD` like upstream); everything else defers to + /// [`DiaryPeriod::today_utc`]. + pub fn today(&self) -> DiaryPeriod { + if self.is_weekly_date() { + DiaryPeriod::Day(self.week_start_of(DiaryDate::today_utc())) + } else { + DiaryPeriod::today_utc(self.freq) + } + } + + /// The period one cadence-step after `p`. + pub fn next(&self, p: DiaryPeriod) -> DiaryPeriod { + self.step(p, 1) + } + + /// The period one cadence-step before `p`. + pub fn prev(&self, p: DiaryPeriod) -> DiaryPeriod { + self.step(p, -1) + } + + fn step(&self, p: DiaryPeriod, dir: i64) -> DiaryPeriod { + // Date-mode weekly: the period is a `Day` on a week-start; a step is + // ±7 days (re-snapped so an off-week-start pivot still lands cleanly). + if self.is_weekly_date() { + if let DiaryPeriod::Day(d) = p { + let start = self.week_start_of(d); + return DiaryPeriod::Day(add_days(start, 7 * dir)); + } + } + if dir >= 0 { + p.next() + } else { + p.prev() + } + } + + /// The most recent `week_start` weekday on or before `d`. + fn week_start_of(&self, d: DiaryDate) -> DiaryDate { + let wd = iso_weekday(&d) as i64; // 1..=7 + let back = (wd - self.week_start.iso()).rem_euclid(7); + add_days(d, -back) + } +} + +/// Shift a calendar date by `delta` days (may be negative). +fn add_days(d: DiaryDate, delta: i64) -> DiaryDate { + from_days(to_days(d.year, d.month, d.day) + delta) +} + /// Day-of-week with Monday=1 … Sunday=7 (ISO 8601). fn iso_weekday(d: &DiaryDate) -> u8 { // Zeller-style via days-from-epoch: 1970-01-01 was a Thursday. diff --git a/crates/nuwiki-core/tests/diary.rs b/crates/nuwiki-core/tests/diary.rs index 78545dd..ded92ef 100644 --- a/crates/nuwiki-core/tests/diary.rs +++ b/crates/nuwiki-core/tests/diary.rs @@ -1,7 +1,9 @@ //! Cluster 4 — diary frequency / period primitives. //! Tests the date math for ISO weeks, monthly, yearly periods. -use nuwiki_core::date::{DiaryDate, DiaryFrequency, DiaryPeriod}; +use nuwiki_core::date::{ + DiaryCalendar, DiaryDate, DiaryFrequency, DiaryPeriod, WeekStart, WeeklyStyle, +}; #[test] fn frequency_parses_canonical_strings() { @@ -182,3 +184,95 @@ fn day_next_and_prev_still_work() { DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 11).unwrap()) ); } + +// ===== DiaryCalendar — weekly naming styles + week-start ===== +// 2026-05-25 is a Monday (so 2026-05-27 is a Wednesday, 2026-05-24 a Sunday). + +fn day(y: i32, m: u8, d: u8) -> DiaryPeriod { + DiaryPeriod::Day(DiaryDate::from_ymd(y, m, d).unwrap()) +} + +#[test] +fn weekly_style_and_week_start_parse() { + assert_eq!(WeeklyStyle::parse("date"), WeeklyStyle::Date); + assert_eq!(WeeklyStyle::parse("vimwiki"), WeeklyStyle::Date); + assert_eq!(WeeklyStyle::parse("iso"), WeeklyStyle::Iso); + assert_eq!(WeeklyStyle::parse(""), WeeklyStyle::Iso); // default + assert_eq!(WeeklyStyle::parse("WEEK"), WeeklyStyle::Iso); + // WeekStart::parse is exercised via the calendar tests below; unknown + // names fall back to Monday. + assert_eq!(WeekStart::parse("sunday"), WeekStart::parse("SUNDAY")); + assert_eq!(WeekStart::parse("nonsense"), WeekStart::monday()); +} + +#[test] +fn date_mode_weekly_today_is_a_day() { + let cal = DiaryCalendar::new( + DiaryFrequency::Weekly, + WeeklyStyle::Date, + WeekStart::monday(), + ); + assert!(matches!(cal.today(), DiaryPeriod::Day(_))); +} + +#[test] +fn iso_mode_weekly_today_is_a_week() { + let cal = DiaryCalendar::new( + DiaryFrequency::Weekly, + WeeklyStyle::Iso, + WeekStart::monday(), + ); + assert!(matches!(cal.today(), DiaryPeriod::Week { .. })); +} + +#[test] +fn date_mode_weekly_steps_seven_days_from_the_week_start_monday() { + let cal = DiaryCalendar::new( + DiaryFrequency::Weekly, + WeeklyStyle::Date, + WeekStart::monday(), + ); + // From a Wednesday: snap back to Mon 05-25, then ±7. + assert_eq!(cal.next(day(2026, 5, 27)), day(2026, 6, 1)); + assert_eq!(cal.prev(day(2026, 5, 27)), day(2026, 5, 18)); + // From the Monday itself: no snap, just ±7. + assert_eq!(cal.next(day(2026, 5, 25)), day(2026, 6, 1)); + assert_eq!(cal.prev(day(2026, 5, 25)), day(2026, 5, 18)); + // Stem is the plain date, like upstream. + assert_eq!(cal.next(day(2026, 5, 27)).format(), "2026-06-01"); +} + +#[test] +fn date_mode_weekly_honours_a_sunday_week_start() { + let cal = DiaryCalendar::new( + DiaryFrequency::Weekly, + WeeklyStyle::Date, + WeekStart::parse("sunday"), + ); + // Wed 05-27 snaps back to Sun 05-24, +7 = Sun 05-31. + assert_eq!(cal.next(day(2026, 5, 27)), day(2026, 5, 31)); + assert_eq!(cal.prev(day(2026, 5, 27)), day(2026, 5, 17)); +} + +#[test] +fn iso_mode_weekly_delegates_to_iso_week_stepping() { + let cal = DiaryCalendar::new( + DiaryFrequency::Weekly, + WeeklyStyle::Iso, + WeekStart::monday(), + ); + let wk = DiaryPeriod::week_containing(DiaryDate::from_ymd(2026, 5, 27).unwrap()); + assert_eq!(cal.next(wk), wk.next()); + assert_eq!(cal.prev(wk), wk.prev()); +} + +#[test] +fn daily_calendar_steps_one_day_regardless_of_weekly_fields() { + let cal = DiaryCalendar::new( + DiaryFrequency::Daily, + WeeklyStyle::Date, + WeekStart::parse("sunday"), + ); + assert_eq!(cal.next(day(2026, 5, 27)), day(2026, 5, 28)); + assert_eq!(cal.prev(day(2026, 5, 27)), day(2026, 5, 26)); +} diff --git a/crates/nuwiki-lsp/src/commands.rs b/crates/nuwiki-lsp/src/commands.rs index d45dd27..96131f9 100644 --- a/crates/nuwiki-lsp/src/commands.rs +++ b/crates/nuwiki-lsp/src/commands.rs @@ -402,11 +402,12 @@ fn diary_open_relative( return Ok(None); }; let freq = wiki.config.frequency(); - let today_period = nuwiki_core::date::DiaryPeriod::today_utc(freq); + let calendar = wiki.config.diary_calendar(); + let today_period = calendar.today(); let period = match rel { RelativeDay::Today => today_period, - RelativeDay::Yesterday => today_period.prev(), - RelativeDay::Tomorrow => today_period.next(), + RelativeDay::Yesterday => calendar.prev(today_period), + RelativeDay::Tomorrow => calendar.next(today_period), }; let Some(uri) = crate::diary::uri_for_period(&wiki.config, &period) else { return Ok(None); @@ -469,7 +470,7 @@ fn diary_step(backend: &Backend, args: Vec, forward: bool) -> Result nuwiki_core::date::DiaryFrequency { nuwiki_core::date::DiaryFrequency::parse(&self.diary_frequency) } + + /// The diary navigation policy (frequency + weekly naming/week-start), + /// used by the diary commands to compute today / next / prev. + pub fn diary_calendar(&self) -> nuwiki_core::date::DiaryCalendar { + nuwiki_core::date::DiaryCalendar::new( + self.frequency(), + nuwiki_core::date::WeeklyStyle::parse(&self.diary_weekly_style), + nuwiki_core::date::WeekStart::parse(&self.diary_start_week_day), + ) + } } fn default_diary_rel_path() -> String { @@ -263,6 +285,15 @@ fn default_diary_frequency() -> String { "daily".to_string() } +fn default_diary_weekly_style() -> String { + // nuwiki's original ISO-week labels; opt into `date` for vimwiki parity. + "iso".to_string() +} + +fn default_diary_start_week_day() -> String { + "monday".to_string() +} + fn default_diary_sort() -> String { "desc".to_string() } @@ -288,6 +319,8 @@ fn wiki_defaults() -> WikiDefaults { diary_rel_path: default_diary_rel_path(), diary_index: default_diary_index(), diary_frequency: default_diary_frequency(), + diary_weekly_style: default_diary_weekly_style(), + diary_start_week_day: default_diary_start_week_day(), // Match upstream vimwiki's `diary_caption_level` default of 0 (year // captions at the top level, months one below). Users override per-wiki. diary_caption_level: 0, @@ -306,6 +339,8 @@ struct WikiDefaults { diary_rel_path: String, diary_index: String, diary_frequency: String, + diary_weekly_style: String, + diary_start_week_day: String, diary_caption_level: u8, diary_sort: String, diary_header: String, @@ -519,6 +554,10 @@ struct RawWiki { #[serde(default)] diary_frequency: Option, #[serde(default)] + diary_weekly_style: Option, + #[serde(default)] + diary_start_week_day: Option, + #[serde(default)] diary_caption_level: Option, #[serde(default)] diary_sort: Option, @@ -584,6 +623,8 @@ impl From for WikiConfig { diary_rel_path: r.diary_rel_path.unwrap_or(d.diary_rel_path), diary_index: r.diary_index.unwrap_or(d.diary_index), diary_frequency: r.diary_frequency.unwrap_or(d.diary_frequency), + diary_weekly_style: r.diary_weekly_style.unwrap_or(d.diary_weekly_style), + diary_start_week_day: r.diary_start_week_day.unwrap_or(d.diary_start_week_day), diary_caption_level: r.diary_caption_level.unwrap_or(d.diary_caption_level), diary_sort: r.diary_sort.unwrap_or(d.diary_sort), diary_header: r.diary_header.unwrap_or(d.diary_header), diff --git a/crates/nuwiki-lsp/tests/index_and_config.rs b/crates/nuwiki-lsp/tests/index_and_config.rs index 700c2b3..6e2c5b4 100644 --- a/crates/nuwiki-lsp/tests/index_and_config.rs +++ b/crates/nuwiki-lsp/tests/index_and_config.rs @@ -284,6 +284,8 @@ fn raw_wiki_parses_every_new_key() { "root": "/tmp/w", "index": "home", "diary_frequency": "weekly", + "diary_weekly_style": "date", + "diary_start_week_day": "sunday", "diary_caption_level": 2, "diary_sort": "asc", "diary_header": "Journal", @@ -297,6 +299,8 @@ fn raw_wiki_parses_every_new_key() { let w = &cfg.wikis[0]; assert_eq!(w.index, "home"); assert_eq!(w.diary_frequency, "weekly"); + assert_eq!(w.diary_weekly_style, "date"); + assert_eq!(w.diary_start_week_day, "sunday"); assert_eq!(w.diary_caption_level, 2); assert_eq!(w.diary_sort, "asc"); assert_eq!(w.diary_header, "Journal"); @@ -305,6 +309,23 @@ fn raw_wiki_parses_every_new_key() { assert_eq!(w.list_margin, 2); assert_eq!(w.links_space_char, "_"); assert!(w.auto_toc); + + // The parsed keys drive a date-mode, Sunday-start diary calendar: + // a weekly note is the week-start date (YYYY-MM-DD), stepping ±7 days. + use nuwiki_core::date::{DiaryDate, DiaryPeriod}; + let cal = w.diary_calendar(); + let wed = DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 27).unwrap()); + assert_eq!( + cal.next(wed), + DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 31).unwrap()) + ); +} + +#[test] +fn diary_calendar_defaults_to_iso_weeks() { + let cfg = WikiConfig::empty(); + assert_eq!(cfg.diary_weekly_style, "iso"); + assert_eq!(cfg.diary_start_week_day, "monday"); } #[test] diff --git a/development/vimwiki-gap.md b/development/vimwiki-gap.md index c1096e3..9494dc1 100644 --- a/development/vimwiki-gap.md +++ b/development/vimwiki-gap.md @@ -109,13 +109,18 @@ fix site. upstream `0`. _Fix:_ `wiki_defaults()` in `crates/nuwiki-lsp/src/config.rs` now defaults `diary_caption_level: 0` (year captions top-level, months one below), still per-wiki overridable. Test `defaults_carry_vimwiki_per_wiki_keys` updated. -- [ ] **`diary_start_week_day`** — **wrongly removed** in `c63ec67`. The removal - treated it as a server concern and hardwired the weekly diary to ISO-Monday - (`crates/nuwiki-core/src/date.rs` `monday_of_iso_week`), but choosing which - weekday the diary week begins on is a **client-side concern** and it **matters - for parity** — non-Monday users lose the upstream option entirely. _Fix:_ - restore the config key client-side (`lua/nuwiki/config.lua`) and thread the - week-start through to the diary date logic instead of hardcoding Monday. +- [x] **`diary_start_week_day`** — removed in `c63ec67` (weekly diary hardwired + to ISO-Monday). Restored as a **configurable choice** so migrators keep + upstream behaviour while new wikis can use ISO weeks. New per-wiki key + `diary_weekly_style`: `iso` (default — `YYYY-Www`, Monday-based, nuwiki's + original) or `date`/`vimwiki` (`YYYY-MM-DD` of the week-start day, upstream + parity, honouring the restored `diary_start_week_day` = `monday`..`sunday`). + _Impl:_ `nuwiki_core::date::{WeeklyStyle, WeekStart, DiaryCalendar}` own the + today/next/prev logic; `WikiConfig::diary_calendar()` builds it from config; + the diary commands (`commands.rs` `diary_open_relative` + next/prev pivot) use + it. Defaults keep existing nuwiki weekly files working (no breaking change). + Covered by calendar tests in `crates/nuwiki-core/tests/diary.rs` and the + config round-trip in `crates/nuwiki-lsp/tests/index_and_config.rs`. ## P3 — Niche / low impact diff --git a/doc/nuwiki.txt b/doc/nuwiki.txt index 711001b..87b791b 100644 --- a/doc/nuwiki.txt +++ b/doc/nuwiki.txt @@ -182,7 +182,11 @@ match upstream vimwiki. `diary_rel_path` `'diary'` `diary_index` `'diary'` — stem of the diary index page. `diary_frequency` `'daily'` | `'weekly'` | `'monthly'` | `'yearly'` -`diary_caption_level` `1` +`diary_weekly_style` `'iso'` (YYYY-Www) | `'date'`/`'vimwiki'` (week-start + YYYY-MM-DD). Weekly diaries only. +`diary_start_week_day` `'monday'`..`'sunday'`; week start used when + `diary_weekly_style = 'date'`. +`diary_caption_level` `0` `diary_sort` `'desc'` | `'asc'` `diary_header` `'Diary'` `html_path` Path for `:Vimwiki2HTML` output. @@ -582,6 +586,13 @@ The `diary_frequency` per-wiki option controls the cadence: monthly YYYY-MM e.g. `2026-05.wiki` yearly YYYY e.g. `2026.wiki` +For weekly diaries, `diary_weekly_style` chooses the file name: the default +`'iso'` uses the ISO-week label above (always Monday-based); `'date'` (a.k.a. +`'vimwiki'`) names the file after the week-start day's date — e.g. with +`diary_start_week_day = 'monday'`, the week of 2026-05-27 is `2026-05-25.wiki` +— matching upstream vimwiki. `diary_start_week_day` (`'monday'`..`'sunday'`) +only applies in `'date'` mode. + `:NuwikiMakeDiaryNote` opens the entry for the current period at the configured cadence; the yesterday / tomorrow commands step back and forward by one period. `` / `` walk between indexed diff --git a/lua/nuwiki/config.lua b/lua/nuwiki/config.lua index 0da64f4..7ee9a2d 100644 --- a/lua/nuwiki/config.lua +++ b/lua/nuwiki/config.lua @@ -21,6 +21,7 @@ M.defaults = { -- `doc/nuwiki.txt`): -- name, root, file_extension, syntax, index -- diary_rel_path, diary_index, diary_frequency, + -- diary_weekly_style, diary_start_week_day, -- diary_caption_level, diary_sort, diary_header -- html_path, template_path, template_default, template_ext, -- template_date_format, css_name, auto_export, auto_toc,