Skip to content

Commit

Permalink
Add Architecture Guide (#312)
Browse files Browse the repository at this point in the history
  • Loading branch information
davish authored Apr 2, 2023
1 parent 73c3139 commit 1919a33
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 31 deletions.
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Full Calendar is open to contributions!

- If you want to develop locally, make sure to make a symbolic link from `main.css` to `styles.css`. Obsidian expects a css file called `styles.css`, but esbuild will output one named `main.css`.
- You can build the plugin for development by running `npm run dev`.
- The [hot reload plugin](https://github.com/pjeby/hot-reload) makes development a lot easier.

To start getting familiar with the codebase, check out the architecture guide at [`src/README.md`](https://github.com/davish/obsidian-full-calendar/blob/main/src/README.md).
54 changes: 23 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,23 @@
# Obsidian Full Calendar Plugin

![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22obsidian-full-calendar%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json)

Keep your calendar in your vault! This plugin integrates the [FullCalendar](https://github.com/fullcalendar/fullcalendar) library into your Obsidian Vault so that you can keep your ever-changing daily schedule and special events and plans alongside your tasks and notes, and link freely between all of them. Each event is stored as a separate note with special frontmatter so you can take notes, form connections and add context to any event on your calendar.

Full Calendar can pull events from frontmatter on notes, or from event lists in daily notes. Full Calendar also supports read-only ICS and CalDAV remote calendars.

You can find the full documentation [here](https://davish.github.io/obsidian-full-calendar/)!

![Sample Calendar](https://raw.githubusercontent.com/davish/obsidian-full-calendar/main/docs/assets/sample-calendar.png)

The FullCalendar library is released under the [MIT license](https://github.com/fullcalendar/fullcalendar/blob/master/LICENSE.txt) by [Adam Shaw](https://github.com/arshaw). It's an awesome piece of work, and it would not have been possible to make something like this plugin so easily without it.

[![Support me on Ko-Fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/M4M1GQ84A)

## Installation

Full Calendar is available from the Obsidian Community Plugins list -- just search for "Full Calendar" paste this link into your browser: `obsidian://show-plugin?id=obsidian-full-calendar`.

### Manual Installation

You can also head over to the [releases page](https://github.com/davish/obsidian-full-calendar/releases) and unzip the latest release inside of the `.obsidian/plugins` directory inside your vault.

## Contributing

Full Calendar is open to contributions!

- If you want to develop locally, make sure to make a symbolic link from `main.css` to `styles.css`. Obsidian expects a css file called `styles.css`, but esbuild will output one named `main.css`.
- You can build the plugin for development by running `npm run dev`.
- The [hot reload plugin](https://github.com/pjeby/hot-reload) makes development a lot easier.
# Obsidian Full Calendar Plugin

![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22obsidian-full-calendar%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json)

Keep your calendar in your vault! This plugin integrates the [FullCalendar](https://github.com/fullcalendar/fullcalendar) library into your Obsidian Vault so that you can keep your ever-changing daily schedule and special events and plans alongside your tasks and notes, and link freely between all of them. Each event is stored as a separate note with special frontmatter so you can take notes, form connections and add context to any event on your calendar.

Full Calendar can pull events from frontmatter on notes, or from event lists in daily notes. Full Calendar also supports read-only ICS and CalDAV remote calendars.

You can find the full documentation [here](https://davish.github.io/obsidian-full-calendar/)!

![Sample Calendar](https://raw.githubusercontent.com/davish/obsidian-full-calendar/main/docs/assets/sample-calendar.png)

The FullCalendar library is released under the [MIT license](https://github.com/fullcalendar/fullcalendar/blob/master/LICENSE.txt) by [Adam Shaw](https://github.com/arshaw). It's an awesome piece of work, and it would not have been possible to make something like this plugin so easily without it.

[![Support me on Ko-Fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/M4M1GQ84A)

## Installation

Full Calendar is available from the Obsidian Community Plugins list -- just search for "Full Calendar" paste this link into your browser: `obsidian://show-plugin?id=obsidian-full-calendar`.

### Manual Installation

You can also head over to the [releases page](https://github.com/davish/obsidian-full-calendar/releases) and unzip the latest release inside of the `.obsidian/plugins` directory inside your vault.
84 changes: 84 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Plugin Architecture

Obsidian Full Calendar's goal is to give users a robust and feature-ful calendar view into their Obsidian Vault. In addition to displaying and modifying events stored in note frontmatter and daily note bulleted lists, it can also read events from the Internet in CalDAV and ICS format.

Obsidian Full Calendar takes its name from [FullCalendar](https://github.com/fullcalendar/fullcalendar), a "Full-sized drag & drop event calendar in JavaScript." This plugin uses FullCalendar as its calendar view. While the naming can be ambiguous, this document will always refer to the FullCalendar view library without any spaces, or as `fullcalendar.io`. The plugin will be referred to either as "the plugin", "Full Calendar" with a space, or "Obsidian Full Calendar".

As of now, the plugin supports events from the following sources and formats:

- Frontmatter of notes in the open Obsidian Vault.
- Bullet list items in Daily Notes generated by the [core Daily Notes](https://help.obsidian.md/Plugins/Daily+notes) or [Periodic Notes](https://github.com/liamcain/obsidian-periodic-notes) plugins.
- ICS files publicly accessible at a URL.
- CalDAV servers authenticated with HTTP basic authentication.

Much of the code of Full Calendar exists to deal with the normalization of these formats so they can be handled by the view layer without worrying about what sources different events are actually from.

Below is a birds-eye view of the different components of the plugin, and the interactions between them.

```
┌──────────────┐
│ │
┌────────────┐ │ │ ┌─────────────┐ ┌──────────────┐
│ ├───► ├───────► ├─────► │
│ EventStore │ │ EventCache │ │ view.ts + │ │ FullCalendar │
│ ◄───┤ ◄───────┤ calendar.ts ◄─────┤ View* │
└────────────┘ │ │ │ │ │ │
│ │ └─────────────┘ └──────────────┘
┌──►└──────▲──┬────┘
│ │ │
│ │ │
│ ┌─────┴──▼─────┐
│ │ │
│ │ Calendars │
│ │ │
│ └─▲──┬─────▲───┘
│ │ │ │
┌───────────┴──────┴──▼──┐ │
│ │ ┌┴────────────┐
│Obsidian Vault APIs* │ │ The │
│ │ │ Internet* │
└────────────────────────┘ └─────────────┘
* Components with an asterisk are not part of the plugin's code.
```

## Codemap

Following the advice in [this blog post on architecture docs](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html), the following section will list out the main modules of the code without linking out to specific file locations that may quickly become stale. Make use of code search and the TypeScript Language Server to jump around and explore the code, with this section as your guide.

### `types`

This module defines some common types used throughout the code. The most prevalent is `OFCEvent`, short for Obsidian Full Calendar Event, that specifies the intermediate representation for all events in the plugin. Note that FullCalendar.io uses a different event format called `EventInput`, which you can read about [in their documentation](https://fullcalendar.io/docs/event-parsing).

Translation between `OFCEvent` and `EventInput` is handled in `interop.ts`. Each `Calendar` subclass (see below) handles its own translation between its source format and `OFCEvent`.

Objects can be validated as OFCEvents using `validateEvent()` . This function is used throughout the code to ensure that only valid events are present.

### `core`

The `core` directory consists of two classes, `EventStore` and `EventCache`. These two classes comprise the plugin's main event-managing logic.

The `EventStore` is the source of truth for events in the plugin. Its interface is similar to a simplified database that stores events, calendars and file locations. Files and calendars are one-to-many relationships: every event is related to exactly one calendar and at most one file, but calendars and files can have many events within them. The `EventStore` allows for effecient querying of events grouped by calendars and files. Every event in the `EventStore` has an ID associated with it. Local events have random IDs that are generated at insert time, but remote events using the iCal spec have `UID`s that are plumbed through.

The `EventCache` manages the state stored in the `EventStore`. Its main job is co-ordinating with both the view layer and the `Calendar`s which perform I/O to actually read events from disk or the network. The `EventCache` has two main hooks to update the `EventStore`:

- Hook (via `MetadataCache.on('update')`) for when a file has changed so that it can tell `Calendar`s to re-parse that file.
- Hook for when an event with a given ID has been modified from the view.
Other components can subscribe to state updates on the `EventCache`. Right now, the view is the only subscriber, but in the future it may be possible for other plugins to subscribe to updates.

Notably, while the `core` components have some knowledge of Obsidian APIs (mostly the `TFile` type and the ability to show `Notice` toasts to the user), they do not hold references to the `App`, `Vault`, `MetadataCache` or any other API that deals with file I/O. File I/O is handled entirely by the `Calendar` subclasses. This simplifies testing dramatically, since the Obsidian API does not need to be mocked out when testing the `EventCache` logic.

The plugin has exactly one `EventCache` instance at any given time. It is initialized and hooked up to `Vault` and `MetadataCache` listeners when the plugin is initialized, in `main.ts`.

While calendars that store their events on disk are kept up-to-date with listeners from the Obsidian API, remote calendars are managed with the [Stale-While-Revalidating (SWR)](https://web.dev/stale-while-revalidate/) cache strategy.

### `calendars`

Each source of events has its own `Calendar` subclass that handles the relevant I/O operations and parses events into the common format. There are two abstract subclasses: `RemoteCalendar` and `EditableCalendar`. `RemoteCalendar`s should cache their responses from the Internet, and have a method to re-fetch their input as part of the SWR cache strategy.

`EditableCalendar`s are constructed with references to an `ObsidianAdapter` instance that handles all interactions with the Obsidian API. This adapter is useful for testing, since it reduces the surface area of APIs to be mocked from the entire API to a handful of functions that the plugin actually uses. It also allows for useful and safe abstractions on top of the Obsidian API, so that its harder for Calendars to do incorrect things, like write a stale copy of a file back to disk.

### `ui`

While `core` and `calendars` make up the Model in the `MVC` pattern, the Views and Controllers are currently both living in the `ui` directory. The view connector to the FullCalendar library lives in `calendar.ts`. Most of the controller logic that interfaces with the `EventCache` lives, somewhat confusingly, in `view.ts`, which also instantiates the Obsidian plugin View. Auxilliary views, like the edit/create modal and settings selectors, are React components that live in their own `.tsx` files and are mounted into the DOM when needed.

**Architecture Invariant**: All interactions with event data should be mediated by the `EventCache`. Code in the `ui` directory should not reference or call out to the `EventStore`, Obsidian Vault APIs, or `Calendar` subclasses.

0 comments on commit 1919a33

Please sign in to comment.