Architecture#

Source layout#

All of the plugin’s logic lives under src/:

File

Responsibility

main.ts

Plugin entry point: loads settings, registers the sidebar/month views, the editor suggest, the live preview extension, and the post-processor; adds the Open calendar in sidebar, Open calendar in a new tab, and Refresh calendars commands.

types.ts

Shared types: CalDavEvent, CalDavCalendar, CalDavAccount, CalDavTimezone, HarangCalendarSettings, CalendarScope, HrcalScope, NoteEvent, CalendarListItem.

settings.ts / settingsTab.ts

Default settings and the settings UI. settingsTab.ts implements Obsidian’s newer declarative getSettingDefinitions() API (1.13.0+) exclusively, with no legacy imperative display() fallback - safe because the plugin’s declared minAppVersion is 1.13.4.

i18n.ts

Looks up the current Obsidian UI language via the official getLanguage() API and returns matching strings from a small ko/en dictionary (falling back to English).

caldav/client.ts

A minimal read-only CalDAV client: calendar discovery (PROPFIND/current-user-principal/calendar-home-set) and event fetching (REPORT calendar-query, optionally windowed to a time range), over Obsidian’s requestUrl.

caldav/ics.ts

An iCalendar (RFC 5545) parser extracting VEVENT properties into a CalDavEvent, skipping VALARM/other sub-components so their properties don’t leak into the parent event.

caldav/recurrence.ts

Expands a CalDavEvent with an RRULE into concrete occurrences within a range, via the rrule package (EXDATE entries excluded).

caldav/timezone.ts

Resolves an account’s configured time zone (an IANA zone name via Intl, or a fixed UTC offset) and lists known IANA zones for the settings dropdown.

caldav/store.ts

CalendarStore: merges and caches events from every configured account/calendar, with TTL-based staleness, on-demand gap-filling for the month view’s range navigation, and scope/search/lookup helpers.

editorSuggest/HrcalEditorSuggest.ts, dateCandidates.ts

Triggers on {{hrcal:. A single staged suggester (no separate @date[/@event[ triggers): it splits the typed query on : to figure out which stage it’s in - account name (searched by name, resolved to that account’s id), then that account’s calendar name, then a combined date-candidate/event-title search - and inserts {{hrcal:<accountId>:<calendarName>:date:YYYY-MM-DD}} or {{hrcal:<accountId>:<calendarName>:event:<uid>}}.

render/frontmatterScope.ts

Reads a note’s harang-account/harang-calendar frontmatter into a CalendarScope. Currently unused - nothing calls this since {{hrcal:...}} references name their account/calendar directly instead of relying on note frontmatter.

notes/noteEvents.ts

Scans the vault for harang-date/harang-repeat/harang-time frontmatter and expands each into NoteEvent occurrences inside a range - purely local to Obsidian, unrelated to any CalDAV server. Re-derives its own all-day-safe RRULE expansion rather than reusing caldav/recurrence.ts (see below). Unrelated to frontmatterScope.ts above - different frontmatter keys, different purpose.

render/dateWidget.ts

Builds the {{hrcal:...:date:YYYY-MM-DD}} card widget: a heading, a warning if the embedded account id/calendar name doesn’t match a registered one, and that day’s events from that specific account/calendar as clickable rows.

render/eventChip.ts / render/eventCard.ts

The inline event chip, and the click-to-open detail popup (a manually positioned floating panel, closed on outside click or Esc). The lookup is scoped to the accountId/calendarName embedded in the {{hrcal:<accountId>:<calendarName>:event:<uid>}} reference itself - the reference already names one exact event.

render/livePreview.ts

A CodeMirror 6 ViewPlugin that replaces {{hrcal:...}} ranges (both date and event kinds, via one regex) with widget/chip widgets in Live Preview, skipping ranges the cursor or selection currently overlaps so the raw syntax stays editable.

render/postProcessor.ts

A Markdown post-processor that does the same replacement for Reading view, by scanning rendered text nodes directly - {{...}} isn’t wikilink syntax, so unlike the old [[cal:...]]/[[event:...]] syntax it’s never pre-parsed into an a.internal-link by Obsidian, and Live Preview and Reading view now share the exact same raw-text matching path.

view/agenda.ts, view/AgendaItemView.ts, vue/AgendaView.vue

The sidebar agenda list: a fixed 30-day window of CalendarListItem (CalDAV events and note events merged) grouped by local day, rendered by a Vue 3 component the ItemView hosts.

view/monthGrid.ts, view/MonthItemView.ts, vue/MonthView.vue

The full-tab month grid view (same CalendarListItem merge as the agenda list), including keyboard (arrow-key) date navigation and cross-month boundary handling.

view/MonthPickerModal.ts

The year/month picker opened from the month view’s heading, with a click-to-edit year field.

Data flow#

settingsTab.ts          -->  CalDavAccount[] (server URL, credentials, calendars)
     |
     v
caldav/client.ts         -->  PROPFIND/REPORT over requestUrl
     |                        (discovery + event fetch)
     v
caldav/ics.ts              -->  parses each VEVENT into a CalDavEvent
     |
     v
caldav/recurrence.ts         -->  expands RRULE occurrences on demand
     |
     v
caldav/store.ts                -->  merged, cached, scoped CalDavEvent[]
     |
     +--> editorSuggest/*.ts          -->  autocomplete while typing
     |
     +--> render/livePreview.ts       -->  widget/chip (Live Preview)
     |
     +--> render/postProcessor.ts     -->  widget/chip (Reading view)
     |        |
     |        v
     |  render/eventChip.ts + eventCard.ts  -->  click-to-open detail popup
     |
     +--> view/AgendaItemView.ts / vue/AgendaView.vue   -->  sidebar list
     |
     +--> view/MonthItemView.ts / vue/MonthView.vue     -->  month grid

notes/noteEvents.ts (vault-wide frontmatter scan) --> NoteEvent[]
     |
     +--> (merged into a CalendarListItem[] alongside CalDavEvent[],
           inside AgendaItemView.ts/MonthItemView.ts, right before
           the sidebar list / month grid above render - nothing else
           in this diagram ever sees a NoteEvent)

Reference syntax#

{{hrcal:<accountId>:<calendarName>:date:YYYY-MM-DD}} renders as a date widget; {{hrcal:<accountId>:<calendarName>:event:<uid>}} renders as an event chip. Both are normally inserted by the staged {{hrcal: editor suggest, but either can be typed by hand too — an unresolvable reference renders as a faded, dashed chip (or, for a date whose account/calendar doesn’t match a registered one, a warning inside the widget) instead of failing silently.

Both kinds are always scoped by the accountId/calendarName named directly in the reference - note frontmatter (harang-account/ harang-calendar, see Usage) plays no role here any more. accountId is the account’s stable internal id rather than its display name, generated once when the account is created and never changing, so renaming an account in Settings does not affect references inserted after this id-based scheme took effect; calendarName is still the calendar’s display name, so a calendar rename can still break existing references, the same as before this change. There is no id-or-name fallback, so a reference written before this change — using the account’s old name-based segment — will no longer resolve and needs to be deleted and re-inserted via the {{hrcal: editor suggest.

Note events (harang-date/harang-repeat/harang-time)#

Unrelated to the reference syntax above: a note carrying harang-date frontmatter (see Usage) becomes a NoteEvent and shows up as an item in the sidebar agenda list and month grid only - never in a {{hrcal:...}} date widget or as an event chip, and never written to or read from a CalDAV server. harang-repeat reuses the same rrule package as caldav/recurrence.ts, but with its own from-scratch expansion logic (see notes/noteEvents.ts above) rather than sharing that module’s function directly, since a note event has no time-of-day at all by default and needs consistently UTC-midnight-anchored date math throughout to avoid the local/UTC all-day mismatch this project has hit (and fixed) several times elsewhere. harang-time layers an optional HH:MM-HH:MM display range on top of that all-day date math - it only affects how a NoteEvent is labeled and sorted alongside timed CalDAV events, not the (still UTC-midnight-anchored) date expansion itself.

No runtime dependencies beyond rrule#

Beyond what Obsidian itself provides (the obsidian package, CodeMirror 6, Vue 3) and the rrule package for recurrence expansion, the plugin has no runtime dependencies — the CalDAV client and iCalendar parser are both hand-written rather than pulled in from npm, keeping the bundle small and avoiding exposure to a third-party HTTP/XML parsing library’s own vulnerabilities.