Skip to content

Commit

Permalink
Merge pull request #28 from renehernandez/highlight-improvements
Browse files Browse the repository at this point in the history
Highlight improvements
  • Loading branch information
renehernandez authored Apr 15, 2021
2 parents f528eae + d2866f3 commit 48919dc
Show file tree
Hide file tree
Showing 25 changed files with 600 additions and 148 deletions.
97 changes: 62 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,33 @@

An experimental plugin to synchronize [Readwise](https://readwise.io) highlights into your Obsidian Vault.

## How the sync process work

The plugin will sync from Readwise only the new highlights since it was last time executed (or since it was installed). The process works as follows:

1. Check if there is a file with the same name.
1. If not, it creates a new file using the template from `Custom Note Header Template` or the default template
2. Read the content of the note, and add the highlights if they are not found. The search for highlight is based on the `highlight_id` from Readwise and not the text of the highlight. The exact match the plugin looks for is of the form `%% highlight_id: <highlight_id> %%`
## Features at glance

### Limitations
- Sync highlights on Obsidian startup
- Update existing notes with new highlights
- Customization for note header and highlights through templating

* It can only pull the most recent 1000 highlights from Readwise (should be solved eventually as part of the implementation for this issue: [issues/7](https://github.com/renehernandez/obsidian-readwise/issues/7)
* It doesn't handle the note associated with a highlight [issues/14](https://github.com/renehernandez/obsidian-readwise/issues/14)
* Customization of how each highlight is stored in the note through another template option [issues/15](https://github.com/renehernandez/obsidian-readwise/issues/15)
## Usage

## Installation
After installation, it will ask for an [API token](https://readwise.io/access_token). This is required in order to pull the highlights from Readwise into your vault.

### From within Obsidian
If you don't configure the API token on installation, you can always configure it on the Settings section.

You can install this plugin from `Settings > Community Plugins > Readwise`.
**NOTE:** The token is stored using [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and it may have conflicts if the same vault were to be open on 2 different windows.

### Manual installation
### Commands

Download zip archive from GitHub releases page. Extract the archive into `<vault>/.obsidian/plugins`.
`Readwise: Sync highlights`: Will pull any new highlights from Readwise since the last time it was synced.

## Usage
### Templating

After installation, it will ask for an [API token](https://readwise.io/access_token). This is required in order to pull the highlights from Readwise into your vault.
The plugin supports templating the header of a note and each individual highlight. Templates are only evaluated during note creation and when adding new highlights.

**NOTE:** The token is stored using [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and it may have conflicts if the same vault were to be open on 2 different windows.
The templating system in use is [Nunjucks](https://mozilla.github.io/nunjucks/).

### Templating
#### Header Template

The built-in template will generate a note with the following structure:
The default header template is:

```markdown
- **URL:** {{ source_url }}
Expand All @@ -44,32 +38,65 @@ The built-in template will generate a note with the following structure:
---
```

This can be overwritten by configuring the `Custom Note Header Template` setting to point to a different template. The templating system in use is [Nunjucks](https://mozilla.github.io/nunjucks/).
This can be overwritten by configuring the `Custom Header Template Path` setting to the path of a different template. The available parameters for a custom header template are:

- `title`
- `source_url`
- `author`
- `category`
- `updated`

#### Highlight Template

The default highlight template is:

```markdown
{{ text }} %% highlight_id: {{ id }} %%
{%- if note %}
Note: {{ note }}
{%- endif %}
```

This can be overwritten by configuring the `Custom Highlight Template Path` setting to the path of a different template. The available parameters for a custom highlight template are:

- `text`
- `note`
- `id`
- `location`

The available parameters for the templates are:
If the custom highlight template doesn't include `highlight_id: <id>`, then this will be appended at the end of the rendered content as `%% highlight_id: <id> %%` (<id> will be replaced by the actual highlight's id).

- title
- source_url
- author
- category
- updated
**Note:** You can find examples of custom templates under [tests/data](./tests/data) folder.

### Settings

- `Readwise API Token`: Add/update your Readwise API token.
- `Sync on startup`: If enabled, will sync highlights from Readwise when Obsidian starts
- `Custom Note Header Template Path`: Path to override default template for Readwise notes
- `Custom Header Template Path`: Path to template note that overrides how the note header is written
- `Custom Highlight Template Path`: Path to template note that overrides how the highlights are written
- `Disable Notifications`: Toggle for pop-up notifications

### Commands
## Installation

`Readwise: Sync highlights`: Will pull any new highlights from Readwise since the last time it was synced.
### From within Obsidian

### Features
You can install this plugin from `Settings > Community Plugins > Readwise`.

- Sync highlights on Obsidian startup
- Update existing notes with new highlights
- Support templating note's header
### Manual installation

Download zip archive from GitHub releases page. Extract the archive into `<vault>/.obsidian/plugins`.

## How the sync process work

The plugin will sync from Readwise only the new highlights since the last time it was executed (or since it was installed). The process works as follows:

1. Check if there is a file with the same name (it checks for notes in top level of the vault only. Issue [#22](https://github.com/renehernandez/obsidian-readwise/issues/22) tracks expanding support for customizing the location.
1. If not, it creates a new file using the template from `Custom Note Header Template` or the default template.
2. Read the content of the note, and add the highlights if they are not found. The search for highlight is based on the `highlight_id` from Readwise and not the text of the highlight. The exact match the plugin looks for is of the form `highlight_id: <id>` where <id> is the actual id of the current highlight being rendered.

### Limitations

* It can only pull the most recent 1000 highlights from Readwise (should be solved eventually as part of the implementation for this issue: [issues/7](https://github.com/renehernandez/obsidian-readwise/issues/7)

### Compatibility

Expand Down
30 changes: 20 additions & 10 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { Result } from "../result";
import Log from "../log";
import type { IDocument, IHighlight } from "./raw_models";
import { Document, Highlight } from "./models";
import type { IDateFactory } from "src/date";

export class ReadwiseApi {
private token: string;
private dateFactory: IDateFactory;

constructor(token: string) {
constructor(token: string, factory: IDateFactory) {
this.token = token;
this.dateFactory = factory;
}

async getDocumentsWithHighlights(
Expand Down Expand Up @@ -50,16 +53,19 @@ export class ReadwiseApi {
page_size: "1000",
};

let moment = (window as any).moment;

if (this.isValidTimestamp(since)) {
Object.assign(params, {
updated__gt: moment(since).utc().format(),
updated__gt: this.dateFactory
.createHandler(since)
.utc()
.format(),
});
}

if (this.isValidTimestamp(to)) {
Object.assign(params, { updated__lt: moment(to).utc().format() });
Object.assign(params, {
updated__lt: this.dateFactory.createHandler(to).utc().format(),
});
}

url += "?" + new URLSearchParams(params);
Expand All @@ -75,7 +81,8 @@ export class ReadwiseApi {
if (response.ok) {
const content = await response.json();
const documents = Document.Parse(
content.results as IDocument[]
content.results as IDocument[],
this.dateFactory
);
if (documents.length > 0) {
Log.debug(
Expand Down Expand Up @@ -105,16 +112,19 @@ export class ReadwiseApi {
page_size: "1000",
};

let moment = (window as any).moment;

if (this.isValidTimestamp(since)) {
Object.assign(params, {
updated__gt: moment(since).utc().format(),
updated__gt: this.dateFactory
.createHandler(since)
.utc()
.format(),
});
}

if (this.isValidTimestamp(to)) {
Object.assign(params, { updated__lt: moment(to).utc().formata() });
Object.assign(params, {
updated__lt: this.dateFactory.createHandler(to).utc().format(),
});
}

url += "?" + new URLSearchParams(params);
Expand Down
9 changes: 5 additions & 4 deletions src/api/models.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { IDateFactory } from "src/date";
import type { IDocument, IHighlight } from "./raw_models";

export class Document {
Expand All @@ -12,19 +13,19 @@ export class Document {

public highlights: Highlight[];

constructor(raw: IDocument) {
constructor(raw: IDocument, factory: IDateFactory) {
this.id = raw.id;
this.title = raw.title;
this.author = raw.author;
this.num_highlights = raw.num_highlights;
this.updated = (window as any).moment(raw.updated).format("YYYY-MM-DD");
this.updated = factory.createHandler(raw.updated).format("YYYY-MM-DD");
this.highlights_url = raw.highlights_url;
this.source_url = raw.source_url;
this.category = raw.category;
}

static Parse(idocs: IDocument[]): Document[] {
return Array.from(idocs).map((idoc) => new Document(idoc));
static Parse(idocs: IDocument[], factory: IDateFactory): Document[] {
return Array.from(idocs).map((idoc) => new Document(idoc, factory));
}
}

Expand Down
29 changes: 29 additions & 0 deletions src/date/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { IDateFactory, IDateHandler } from "./interface";

export class DateHandler implements IDateHandler {
private moment: any;
private date: any;

constructor(date: any) {
this.moment = window.moment;
this.date = date;
}

fromNow(): string {
return this.moment(this.date).fromNow();
}

format(format?: string): string {
return this.moment(this.date).format(format);
}

utc(): IDateHandler {
return new DateHandler(this.moment(this.date).utc());
}
}

export class DateFactory implements IDateFactory {
createHandler(date: any): IDateHandler {
return new DateHandler(date);
}
}
2 changes: 2 additions & 0 deletions src/date/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { IDateFactory, IDateHandler } from "./interface";
export { DateHandler, DateFactory } from "./handler";
11 changes: 11 additions & 0 deletions src/date/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface IDateHandler {
fromNow(): string;

format(format?: string): string;

utc(): IDateHandler;
}

export interface IDateFactory {
createHandler(date: any): IDateHandler;
}
20 changes: 12 additions & 8 deletions src/fileDoc.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import type nunjucks from "nunjucks";

import type { Document } from './api/models';
import type { Template } from './template';
import Log from "./log";
import type { IFileSystemHandler } from './fileSystem';
import type { HeaderTemplateRenderer, HighlightTemplateRenderer } from "./template";

export class FileDoc {

doc: Document;
template: Template
headerRenderer: HeaderTemplateRenderer
highlightRenderer: HighlightTemplateRenderer
fsHandler: IFileSystemHandler

constructor(doc: Document, template: Template, handler: IFileSystemHandler) {
constructor(doc: Document, header: HeaderTemplateRenderer, highlight: HighlightTemplateRenderer, handler: IFileSystemHandler) {
this.doc = doc;
this.template = template;
this.headerRenderer = header;
this.highlightRenderer = highlight;
this.fsHandler = handler;
}

Expand All @@ -23,16 +27,16 @@ export class FileDoc {
if (!(await this.fsHandler.exists(file))) {
Log.debug(`Document ${file} not found. Will be created`);

content = await this.template.templatize(this.doc);
content = await this.headerRenderer.render(this.doc);
}
else {
Log.debug(`Document ${file} found. Updating highlights`);
Log.debug(`Document ${file} found. Loading content and updating highlights`);
content = await this.fsHandler.read(file);
}

this.doc.highlights.forEach(hl => {
if (!content.includes(`%% highlight_id: ${hl.id} %%`)) {
content += `\n${hl.text} %% highlight_id: ${hl.id} %%\n`
if (!content.includes(`highlight_id: ${hl.id}`)) {
content += `\n${this.highlightRenderer.render(hl)}\n`
}
});

Expand Down
4 changes: 2 additions & 2 deletions src/fileSystem/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { IFileSystemHandler as IFileSystemHandler } from "./interface";
export { FileSystemHandler as FileSystemHandler } from "./handler";
export type { IFileSystemHandler } from "./interface";
export { FileSystemHandler } from "./handler";
12 changes: 7 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import type { Document } from './api/models';
import ReadwiseApiTokenModal from "./modals/enterApiToken/tokenModal";
import Log from "./log";
import type { Result } from "./result";
import { Template } from "./template";
import { HeaderTemplateRenderer, HighlightTemplateRenderer } from "./template";
import { FileDoc } from "./fileDoc";
import { TokenManager } from "./tokenManager";
import { FileSystemHandler } from "./fileSystem";
import { DateFactory } from "./date";


export default class ObsidianReadwisePlugin extends Plugin {
Expand All @@ -34,7 +35,7 @@ export default class ObsidianReadwisePlugin extends Plugin {

async onload() {
let statusBarEl = this.addStatusBarItem();
this.statusBar = new StatusBar(statusBarEl, this);
this.statusBar = new StatusBar(statusBarEl, this, new DateFactory());
this.tokenManager = new TokenManager();

await this.loadSettings();
Expand Down Expand Up @@ -114,10 +115,11 @@ export default class ObsidianReadwisePlugin extends Plugin {
async updateNotes(documents: Document[]) {
this.setState(PluginState.syncing)
const handler = new FileSystemHandler(this.app.vault.adapter as FileSystemAdapter);
const template = new Template(this.settings.headerTemplate, handler);
const header = await HeaderTemplateRenderer.create(this.settings.headerTemplatePath, handler);
const highlight = await HighlightTemplateRenderer.create(this.settings.highlightTemplatePath, handler);

documents.forEach(doc => {
const fileDoc = new FileDoc(doc, template, handler);
const fileDoc = new FileDoc(doc, header, highlight, handler);

fileDoc.createOrUpdate();
});
Expand All @@ -142,7 +144,7 @@ export default class ObsidianReadwisePlugin extends Plugin {
}
}

this.api = new ReadwiseApi(token);
this.api = new ReadwiseApi(token, new DateFactory());
return true
}

Expand Down
Loading

0 comments on commit 48919dc

Please sign in to comment.