# Calendar Blocks Calendar Blocks is an Obsidian plugin for rendering dates and date ranges as calendars. ## Examples ### Single date ```` ```date 2026.06.06 ``` ```` A minimal calendar with June 6 selected ### Legend ```` ```dates Vacation: 7 May 2027 - 17 May Birthday: 9 May Conference: 19 May ``` ```` Legend with smart year selection ### Range within one month ```` ```date 2026.06.05 - 2026.06.11 ``` ```` A minimal calendar with a range from June 5 through June 11 ### Range across adjacent months ```` ```date 2026.06.05 - 2026.07.05 ``` ```` Minimal calendars with a range from June 5 through July 5 ### Range with omitted months ```` ```date 2026.06.05 - 2026.10.05 ``` ```` Minimal calendars with a range from June 5 through October 5 and a wave for omitted months ## Usage Use a fenced `date` code block for a single date: ````markdown ```date 2025.07.27 ``` ```` The `dates` block name is supported as an alias and has identical behavior. Use a hyphen between two dates for a range: ````markdown ```date 2025.07.27 - 2025.08.03 ``` ```` ### Multiple ranges and dates Put each range or single date on its own line in the same block: ````markdown ```dates 2026.06.05 - 2026.06.11 2026.06.09 - 2026.06.15 2026.06.10 2026.07.03 - 2026.08.05 ``` ```` All entries appear together. Ranges and single dates use two independent pools of the same 40-color palette, each starting with red, blue, and green in source order. The first single date and the first range are both red; adding a single date does not advance the range colors, and vice versa. A range with identical endpoints collapses into a single date: it uses a circle, a color from the single-date pool, and a circle in the legend. Each entry keeps its color across weeks and months. Overlapping ranges retain their translucent layers, so their colors blend at intersections. Single dates appear as colored circles above all ranges, regardless of line order. Blank lines are ignored; an invalid entry reports its line number and rejects the whole block. A block accepts up to 40 entries in total; adding a 41st range or single date shows an error. Blank lines do not consume colors or count toward the limit. Months are shown once, in chronological order, adding the endpoint months of each range and the months of single dates. Long ranges still show their first and last months, with a wave at each gap. Additional months form rows of two calendars and stack vertically in narrow panes. ### Legend Add a label followed by a colon before a range or single date: ````markdown ```dates Vacation: 1 Mar - 9 Mar Conference: 5 Mar - 7 Mar Birthday: 8 Mar ``` ```` The legend appears below all calendars, with one labeled entry per line and a marker on the left. Ranges use a pill about 1.47 times as wide as a single-date circle, matching the range's color and transparency. Single dates use solid circles. Markers share a column and align to its right edge, keeping all labels aligned. Unlabeled entries are omitted from the legend but still consume a color from their type's pool. If no entries have labels, no legend is shown. Labels support plain text, including spaces and Unicode. ### Supported date formats Dates accept one- or two-digit days and months: - Year first: `2026.03.02`, `2026-03-02`, `2026/3/2`. - Day first: `02.03.2026`, `2/3/2026` (always day/month/year). - Month names in the system locale and Obsidian language, case-insensitive: full names, abbreviations (with or without a trailing period), and grammatical forms used in dates, such as `1 марта`, `1 март`, or `1 мар.` in Russian. English names (`2 Mar 2026`, `2 March 2026`) are always accepted. - Without a year: `2.3`, `02/03`, `2 Mar`. For the first entry, when a single date or both range endpoints omit their years, the plugin uses the source note's creation year (Obsidian's file creation timestamp). It does not use the current year or the last modification year. If both range years are omitted, the creation year determines the start year and the end may roll into the next year. If the source note is unavailable, specify the year of the first single date or either endpoint of the first range explicitly. A range end without a year uses the start's year, advancing to the next year when its month/day would fall before the start. For example, `28 Dec 2005 - 1 Jan` ends on January 1, 2006, independently of the note's creation year. Conversely, `28 Dec - 1 Jan 2006` starts on December 28, 2005: an omitted start year uses the end's year, moving back one year if its month/day would fall after the end. Explicit years are always respected. For subsequent entries, a single date without a year (or the start of a range with both years omitted) uses the year that places it closest to the interval from the earliest to the latest previously resolved date in the block. Dates inside that interval have zero distance; ties choose the earliest occurrence. This takes precedence over the note creation year. Each resolved entry expands the interval, and blank lines do not reset it. A yearless February 29 uses the nearest valid leap year. Explicit years and inference from the other endpoint of the same range retain priority. For example, `Vacation: 7 May 2027 - 17 May` followed by `Birthday: 9 May` puts all dates in 2027. With `30 Dec 2010 - 7 Jan`, the range ends in 2011; a following `9 Jan` resolves to 2011, while `5 Dec` resolves to 2010. For example, in a note created in 2026: ````markdown ```date 1 Mar - 4 Apr ``` ```` This selects March 1 through April 4, 2026. Formats can be mixed within a range; hyphens, en dashes and em dashes are accepted as separators. Invalid calendar dates and reversed ranges with explicit years are rejected. Ranges crossing New Year can omit the end year, for example `31 Dec 2026 - 1 Jan`. ## Settings Calendars use the grid-free Minimal design with narrow weekday labels and a filled circle for a single selected date. Enable **Stretch calendar** in the Calendar Blocks settings to make calendars use the full available note width. It is disabled by default, preserving the compact layout. **Show today** is enabled by default. It marks the current local date with a red, hand-drawn open oval in any displayed month and refreshes after the day changes. The marker is chosen pseudorandomly from four variants with different gaps and stroke thicknesses. The current local date, the source filename (including its extension), and the date/dates block's order in the note determine the choice. Rerendering, resizing, or switching settings keeps the same variant for that combination. Block order comes from the source text, independently of render order. Turn it off to hide the mark. The editable SVG resource is [`src/assets/today-marker.svg`](src/assets/today-marker.svg); it is embedded in the plugin bundle during the build, alongside `today-marker-2.svg`, `today-marker-3.svg`, and `today-marker-4.svg`. ## Installation ### Manual installation from Releases 1. Open the [latest release](https://github.com/pochemuto/calendar-blocks/releases/latest) and download the attached `main.js`, `manifest.json`, and `styles.css` files. Do not download GitHub's automatically generated source code archives. 2. Create this directory inside your Obsidian vault: ```text /.obsidian/plugins/calendar-blocks/ ``` 3. Copy the three downloaded files directly into that directory. The resulting layout must be: ```text /.obsidian/plugins/calendar-blocks/ ├── main.js ├── manifest.json └── styles.css ``` 4. Reload Obsidian, open **Settings → Community plugins**, and enable **Calendar Blocks**. ## Build ```sh npm ci npm run build ``` Copy the contents of `build/` to: ```text /.obsidian/plugins/calendar-blocks/ ``` Then reload Obsidian and enable **Calendar Blocks** in the community plugins settings. ## Publishing Before publishing, run `npm test`, `npm run build`, and `git diff --check`. Keep the version in `manifest.json`, `package.json`, and `package-lock.json` in sync, and record its minimum Obsidian version in `versions.json`. The minimum supported Obsidian version is 1.5.7, which introduced the `Vault.getFileByPath` API used to resolve the source note. Create a Git tag that exactly matches the manifest version, without a `v` prefix (for example, `0.1.1`). Pushing that tag runs CI and publishes a GitHub release with only `main.js`, `manifest.json`, and `styles.css` as attached assets. Append a concise, readable bullet to [CHANGELOG.md](CHANGELOG.md) when committing a change. Keep it as a flat list, with one entry per line and no version sections. CI generates the release description from added bullet lines in the changelog diff since the previous release tag, preserving file order and removing duplicates. The first release, or the first release with a changelog, includes all its entries. Both current tags (`0.1.1`) and legacy tags (`v0.1.0`) delimit the diff. Before publishing the release, CI generates GitHub build provenance attestations for the three individual plugin assets. The signing permissions are restricted to the release job, which runs only for version tags after tests and the build succeed. Verify downloaded assets with the GitHub CLI: ```sh gh attestation verify main.js --repo pochemuto/calendar-blocks gh attestation verify manifest.json --repo pochemuto/calendar-blocks gh attestation verify styles.css --repo pochemuto/calendar-blocks ``` The initial directory submission is made through [Obsidian Community](https://community.obsidian.md) after signing in and connecting the repository owner's GitHub account. Subsequent updates use GitHub releases. See the official [submission guide](https://docs.obsidian.md/Plugins/Releasing/Submit%20your%20plugin). ## License [MIT](LICENSE). ## Visual regression tests `npm test` runs unit tests and 56 screenshot comparisons (28 cases in light and dark themes). Visual tests require a running Docker engine. The first run downloads the pinned Playwright image. Local runs and CI use the same Linux arm64 image, Chromium version, bundled Noto Sans font, locale, timezone and pixel density; no host fonts or Obsidian installation are needed. ```sh npm ci npm test # Run just one suite: npm run test:unit npm run test:visual # Filter visual cases: npm run test:visual -- --grep omitted-months ``` Reference PNGs live in `tests/test-data/visual/linux/` and belong in Git. Every pixel must match: both the color threshold and allowed differing pixel count are zero. An additional decoded RGBA comparison includes antialiased pixels that Playwright's comparator can ignore. Missing references fail a normal test run. On failure, `test-results/` contains expected, actual and diff images, and `npm run test:visual:report` opens the HTML report. CI uploads these artifacts. After an intentional visual change, explicitly regenerate references: ```sh npm run test:visual:update # Or update only the affected cases: npm run test:visual:update -- --grep legend ``` Review the changed PNGs before accepting them, then rerun `npm test`. The input fixtures live in `tests/test-data/visual/*.md`. Each scenario has one Markdown file (for example, `adjacent-months.md`) shared by all platforms and themes. It contains one fenced `date` or `dates` block and embeds light and dark reference images for every platform directory using relative links. Screenshot paths follow `/-.png`, for example `linux/adjacent-months-light.png` and `linux/adjacent-months-dark.png`. Open the Markdown file on GitHub to see all inputs and outputs together. The test runner automatically runs each scenario in both light and dark themes; Playwright selects the screenshot directory for the platform it runs on. Optional fixture settings go in YAML frontmatter at the start of the file: ```yaml --- width: 280 locale: ru stretch: true showToday: true --- ``` Defaults are width 640, locale `en-US`, stretching disabled, and Show today enabled. The source note creation year is fixed at 2026, and the browser clock is fixed at October 26, 2026, 12:00 UTC. Marker randomness uses a fixed seed of 7 for each fixture, so today's marker produces stable screenshots. Set `markerSeed` in fixture frontmatter to cover another variant reproducibly. To add a scenario in both themes, add `.md` with the input, settings and image links for all platforms and themes, then run `npm run test:visual:update`. There is no separate case list in code. Unit tests validate the Markdown format, all screenshot embeds and their relative paths, existing PNG targets, and a matching Markdown file for every reference image across every platform directory. Broken, missing, duplicate or swapped image links fail tests. Adding another platform requires its light and dark screenshot embeds in each existing scenario, with no duplicate input files. Keep the Docker image version in `tests/visual/run.mjs` aligned with the exact `@playwright/test` dependency. Browser or font upgrades may require reviewed reference updates. The harness uses the production parser, renderer and stylesheet with fixed Obsidian theme variables. It covers responsive container queries, highlight layers, legends, localization, month lengths, year boundaries, stretching and all 40 selection colors. These tests exercise the shared calendar rendering; Live Preview/Reading integration and third-party Obsidian themes still need manual checking in Obsidian.