Skip to content

Commit

Permalink
feat: sorting highlights (#38)
Browse files Browse the repository at this point in the history
This update includes:
- Add 5 sorting criteria for highlights
- Add basic tests
- Refactor tests to support sorting
- Update documentation with sorting criteria settings and examples
  • Loading branch information
bandantonio committed Jul 29, 2024
1 parent e154122 commit 5ac0b62
Show file tree
Hide file tree
Showing 16 changed files with 740 additions and 186 deletions.
Binary file added docs/assets/example-highlights-order.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
121 changes: 121 additions & 0 deletions docs/guide/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,127 @@ Backup highlights folder before import. The backup folder name is pre-configured
> If the setting is disabled, the plugin will overwrite the contents of the [highlight folder](#highlight-folder) on import.
> This behavior will be improved based on the feedback received: [Issue #34](https://github.com/bandantonio/obsidian-apple-books-highlights-plugin/issues/34#issuecomment-2231429171)
## Highlights sorting criterion

- Default value: By creation date (from oldest to newest)

Sort highlight by a specific criterion.

The available options are:

- By creation date (from oldest to newest)
- By creation date (from newest to oldest)
- By last modification date* (from oldest to newest)
- By last modification date* (from newest to oldest)
- By location in a book

::: tip What a modification is?
Modification includes the following cases:

- Updating highlight text
- Adding or updating a note
- Changing the highlight color or style
:::

::: details Examples

Let's consider an example book with the following highlights (callouts to the left indicate the order in which the highlights were created):

![Highlights order](../assets/example-highlights-order.png)

- **By creation date (from oldest to newest)**: The highlights that were created first will be at the top.

::: details Example
```md
## Annotations
----
- 🎯 Highlight:: And now this. Christmas Day, alone on a hospital ward, failing to get through my shift.
----
- 🎯 Highlight:: At 10:30am, I looked around the ward. Nurse Janice was sprinting up and down corridor A
----
- 🎯 Highlight:: As Christmas turned to Boxing Day, I stayed up poring over my old notes and wondered whether that was where I was going wrong
----
- 🎯 Highlight:: ‘Merry Christmas, Ali. Try not to kill anyone.’
```
:::

- **By creation date (from newest to oldest)**: The highlights that were created last will be at the top.

::: details Example
```md
## Annotations
----
- 🎯 Highlight:: ‘Merry Christmas, Ali. Try not to kill anyone.’
----
- 🎯 Highlight:: As Christmas turned to Boxing Day, I stayed up poring over my old notes and wondered whether that was where I was going wrong
----
- 🎯 Highlight:: At 10:30am, I looked around the ward. Nurse Janice was sprinting up and down corridor A
----
- 🎯 Highlight:: And now this. Christmas Day, alone on a hospital ward, failing to get through my shift.
```
:::

- **By last modification date (from oldest to newest)**: The highlights that were modified first will be at the top.

::: details Example
```md
## Annotations
----
- 🎯 Highlight:: As Christmas turned to Boxing Day, I stayed up poring over my old notes and wondered whether that was where I was going wrong
- 📝 Note:: N/A
----
- 🎯 Highlight:: ‘Merry Christmas, Ali. Try not to kill anyone.’
- 📝 Note:: N/A
----
- 🎯 Highlight:: At 10:30am, I looked around the ward. Nurse Janice was sprinting up and down corridor A
- 📝 Note:: Test modification date (modified first)
----
- 🎯 Highlight:: And now this. Christmas Day, alone on a hospital ward, failing to get through my shift.
- 📝 Note:: Test modification date (modified second)
```
:::

- **By last modification date (from newest to oldest)**: The highlights that were modified last will be at the top.

::: details Example
```md
## Annotations
----
- 🎯 Highlight:: And now this. Christmas Day, alone on a hospital ward, failing to get through my shift.
- 📝 Note:: Test modification date (modified second)
----
- 🎯 Highlight:: At 10:30am, I looked around the ward. Nurse Janice was sprinting up and down corridor A
- 📝 Note:: Test modification date (modified first)
----
- 🎯 Highlight:: ‘Merry Christmas, Ali. Try not to kill anyone.’
- 📝 Note:: N/A
----
- 🎯 Highlight:: As Christmas turned to Boxing Day, I stayed up poring over my old notes and wondered whether that was where I was going wrong
- 📝 Note:: N/A
```
:::

- **By location in a book**: Highlights are sorted by their location in a book.

::: details Example
```md
## Annotations
----
- 🎯 Highlight:: ‘Merry Christmas, Ali. Try not to kill anyone.’
- 📝 Note:: N/A
----
- 🎯 Highlight:: At 10:30am, I looked around the ward. Nurse Janice was sprinting up and down corridor A
- 📝 Note:: Test modification date (modified first)
----
- 🎯 Highlight:: And now this. Christmas Day, alone on a hospital ward, failing to get through my shift.
- 📝 Note:: Test modification date (modified second)
----
- 🎯 Highlight:: As Christmas turned to Boxing Day, I stayed up poring over my old notes and wondered whether that was where I was going wrong
- 📝 Note:: N/A
```
:::
:::

## Template

- Template for highlight files.
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "apple-books-import-highlights",
"name": "Apple Books - Import Highlights",
"version": "1.3.0",
"version": "1.4.0",
"minAppVersion": "0.15.0",
"description": "Import your Apple Books highlights and notes to Obsidian.",
"author": "bandantonio",
Expand Down
128 changes: 64 additions & 64 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,65 +1,65 @@
{
"name": "obsidian-apple-books-highlights-plugin",
"version": "1.3.0",
"description": "Import highlights and notes from your Apple Books to Obsidian",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"lint": "eslint . --ext .ts",
"version": "node version-bump.mjs",
"test": "vitest",
"coverage": "vitest run --coverage",
"db:generate": "drizzle-kit generate:sqlite",
"db:migrate": "npx tsx src/db/migrate.ts",
"db:seed": "npx tsx src/db/seed.ts",
"prepare": "husky",
"docs:dev": "vitepress dev docs --port 3000",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"keywords": [
"obsidian",
"plugin",
"apple",
"books",
"applebooks",
"iBooks",
"highlights",
"notes",
"importer"
],
"author": "bandantonio",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/bandantonio/obsidian-apple-books-highlights-plugin.git"
},
"engines": {
"node": ">=20.8.0",
"npm": ">=10.0.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.10",
"@types/node": "^18.4.1",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"@vitest/coverage-istanbul": "^1.5.1",
"better-sqlite3": "^9.5.0",
"builtin-modules": "3.3.0",
"drizzle-kit": "^0.20.17",
"drizzle-orm": "^0.30.9",
"esbuild": "0.17.3",
"eslint": "^8.57.0",
"husky": "^9.0.11",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4",
"vitepress": "^1.3.1",
"vitest": "^1.5.1"
},
"dependencies": {
"dayjs": "^1.11.10",
"handlebars": "^4.7.8"
}
}
"name": "obsidian-apple-books-highlights-plugin",
"version": "1.4.0",
"description": "Import highlights and notes from your Apple Books to Obsidian",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"lint": "eslint . --ext .ts",
"version": "node version-bump.mjs",
"test": "vitest",
"coverage": "vitest run --coverage",
"db:generate": "drizzle-kit generate:sqlite",
"db:migrate": "npx tsx src/db/migrate.ts",
"db:seed": "npx tsx src/db/seed.ts",
"prepare": "husky",
"docs:dev": "vitepress dev docs --port 3000",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"keywords": [
"obsidian",
"plugin",
"apple",
"books",
"applebooks",
"iBooks",
"highlights",
"notes",
"importer"
],
"author": "bandantonio",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/bandantonio/obsidian-apple-books-highlights-plugin.git"
},
"engines": {
"node": ">=20.8.0",
"npm": ">=10.0.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.10",
"@types/node": "^18.4.1",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"@vitest/coverage-istanbul": "^1.5.1",
"better-sqlite3": "^9.5.0",
"builtin-modules": "3.3.0",
"drizzle-kit": "^0.20.17",
"drizzle-orm": "^0.30.9",
"esbuild": "0.17.3",
"eslint": "^8.57.0",
"husky": "^9.0.11",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4",
"vitepress": "^1.3.1",
"vitest": "^1.5.1"
},
"dependencies": {
"dayjs": "^1.11.10",
"handlebars": "^4.7.8"
}
}
11 changes: 8 additions & 3 deletions src/methods/saveHighlightsToVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'path';
import { ICombinedBooksAndHighlights } from '../types';
import { AppleBooksHighlightsImportPluginSettings } from '../settings';
import { renderHighlightsTemplate } from './renderHighlightsTemplate';
import { sortHighlights } from 'src/methods/sortHighlights';

export default class SaveHighlights {
private app: App;
Expand Down Expand Up @@ -43,9 +44,13 @@ export default class SaveHighlights {

await this.vault.createFolder(this.settings.highlightsFolder);

highlights.forEach(async (highlight: ICombinedBooksAndHighlights) => {
const renderedTemplate = await renderHighlightsTemplate(highlight, this.settings.template);
const filePath = path.join(this.settings.highlightsFolder, `${highlight.bookTitle}.md`);
highlights.forEach(async (combinedHighlight: ICombinedBooksAndHighlights) => {
// Order highlights according to the value in settings
const sortedHighlights = sortHighlights(combinedHighlight, this.settings.highlightsSortingCriterion);

// Save highlights to vault
const renderedTemplate = await renderHighlightsTemplate(sortedHighlights, this.settings.template);
const filePath = path.join(this.settings.highlightsFolder, `${combinedHighlight.bookTitle}.md`);

await this.vault.create(
filePath,
Expand Down
62 changes: 62 additions & 0 deletions src/methods/sortHighlights.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { IHighlight, ICombinedBooksAndHighlights, IHighlightsSortingCriterion } from 'src/types';

export const sortHighlights = (combinedHighlight: ICombinedBooksAndHighlights, highlightsSortingCriterion: string) => {
let sortedHighlights: IHighlight[] = [];

switch (highlightsSortingCriterion) {
case IHighlightsSortingCriterion.CreationDateOldToNew:
sortedHighlights = combinedHighlight.annotations.sort((a, b) => a.highlightCreationDate - b.highlightCreationDate);
break;
case IHighlightsSortingCriterion.CreationDateNewToOld:
sortedHighlights = combinedHighlight.annotations.sort((a, b) => b.highlightCreationDate - a.highlightCreationDate);
break;
case IHighlightsSortingCriterion.LastModifiedDateOldToNew:
sortedHighlights = combinedHighlight.annotations.sort((a, b) => a.highlightModificationDate - b.highlightModificationDate);
break;
case IHighlightsSortingCriterion.LastModifiedDateNewToOld:
sortedHighlights = combinedHighlight.annotations.sort((a, b) => b.highlightModificationDate - a.highlightModificationDate);
break;
case IHighlightsSortingCriterion.Book:
sortedHighlights = combinedHighlight.annotations.sort((a, b) => {
const firstHighlightLocation = highlightLocationToNumber(a.highlightLocation);
const secondHighlightLocation = highlightLocationToNumber(b.highlightLocation);

return compareLocations(firstHighlightLocation, secondHighlightLocation);
});
break;
}

return {
...combinedHighlight,
annotations: sortedHighlights
};
}

// The function converts a highlight location string to an array of numbers
export const highlightLocationToNumber = (highlightLocation: string): number[] => {
// epubcfi example structure: epubcfi(/6/2[body01]!/4/2/2/1:0)
return highlightLocation

Check warning on line 38 in src/methods/sortHighlights.ts

View workflow job for this annotation

GitHub Actions / ✅ Linter

Forbidden non-null assertion

Check warning on line 38 in src/methods/sortHighlights.ts

View workflow job for this annotation

GitHub Actions / ✅ Linter

Forbidden non-null assertion
.slice(8, -1) // Get rid of the epubcfi() wrapper
.split(',') // Split the locator into three parts: the common parent, the start subpath, and the end subpath
.slice(0, -1) // Get rid of the end subpath (third part)
.join(',') // Join the first two parts back together
.match(/(?<!\[)[/:]\d+(?!\])/g)! // Extract all the numbers (except those in square brackets) from the first two parts
.map(match => parseInt(match.slice(1))) // Get rid of the leading slash or colon and convert the string to a number
}

// The function performs lexicographic comparison of two locations to determine their relative position in a book
export const compareLocations = (firstLocation: number[], secondLocation: number[]) => {
// Loop through each element of both arrays up to the length of the shorter one
for (let i = 0; i < Math.min(firstLocation.length, secondLocation.length); i++) {
if (firstLocation[i] < secondLocation[i]) {
return -1;
}
if (firstLocation[i] > secondLocation[i]) {
return 1;
}
}

// If the loop didn't return, the arrays are equal up to the length of the shorter array
// so the function returns the difference in lengths to determine the order of the corresponding locations
return firstLocation.length - secondLocation.length;
}
Loading

0 comments on commit 5ac0b62

Please sign in to comment.