diff --git a/docs/assets/example-highlights-order.png b/docs/assets/example-highlights-order.png
new file mode 100644
index 0000000..66c9462
Binary files /dev/null and b/docs/assets/example-highlights-order.png differ
diff --git a/docs/guide/settings.md b/docs/guide/settings.md
index 83078b5..7ece6b0 100644
--- a/docs/guide/settings.md
+++ b/docs/guide/settings.md
@@ -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.
diff --git a/manifest.json b/manifest.json
index 0928c04..b951421 100644
--- a/manifest.json
+++ b/manifest.json
@@ -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",
diff --git a/package.json b/package.json
index d577101..0efd1b0 100644
--- a/package.json
+++ b/package.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/src/methods/saveHighlightsToVault.ts b/src/methods/saveHighlightsToVault.ts
index 0c28f21..b55ea31 100644
--- a/src/methods/saveHighlightsToVault.ts
+++ b/src/methods/saveHighlightsToVault.ts
@@ -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;
@@ -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,
diff --git a/src/methods/sortHighlights.ts b/src/methods/sortHighlights.ts
new file mode 100644
index 0000000..fec9014
--- /dev/null
+++ b/src/methods/sortHighlights.ts
@@ -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
+ .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(/(? 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;
+}
diff --git a/src/settings.ts b/src/settings.ts
index a508f1d..3e51e5d 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -1,11 +1,13 @@
import { App, Notice, PluginSettingTab, Setting } from 'obsidian';
import IBookHighlightsPlugin from '../main';
import defaultTemplate from './template';
+import { IHighlightsSortingCriterion, IBookHighlightsPluginSettings } from './types';
-export class AppleBooksHighlightsImportPluginSettings {
+export class AppleBooksHighlightsImportPluginSettings implements IBookHighlightsPluginSettings {
highlightsFolder = 'ibooks-highlights';
backup = false;
importOnStart = false;
+ highlightsSortingCriterion = IHighlightsSortingCriterion.CreationDateOldToNew;
template = defaultTemplate;
}
@@ -66,6 +68,31 @@ export class IBookHighlightsSettingTab extends PluginSettingTab {
});
});
+ new Setting(containerEl)
+ .setName('Highlights sorting criterion')
+ .setDesc('Sort highlights by a specific criterion. Default: By creation date (from oldest to newest)')
+ .setClass('ibooks-highlights-sorting')
+ .addDropdown((dropdown) => {
+ const options = {
+ [IHighlightsSortingCriterion.CreationDateOldToNew]: 'By creation date (from oldest to newest)',
+ [IHighlightsSortingCriterion.CreationDateNewToOld]: 'By creation date (from newest to oldest)',
+ [IHighlightsSortingCriterion.LastModifiedDateOldToNew]: 'By modification date (from oldest to newest)',
+ [IHighlightsSortingCriterion.LastModifiedDateNewToOld]: 'By modification date (from newest to oldest)',
+ [IHighlightsSortingCriterion.Book]: 'By location in a book'
+ };
+
+ dropdown
+ .addOptions(options)
+ .setValue(this.plugin.settings.highlightsSortingCriterion)
+ .onChange(async (value: IHighlightsSortingCriterion) => {
+ console.log('value', value);
+
+ this.plugin.settings.highlightsSortingCriterion = value;
+ await this.plugin.saveSettings();
+ }
+ );
+ });
+
new Setting(containerEl)
.setName('Template')
.setDesc('Template for highlight files')
diff --git a/src/types.ts b/src/types.ts
index 5e2640f..8f52b33 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,3 +1,5 @@
+import { Template } from 'handlebars';
+
export interface IBook {
ZASSETID: string;
ZTITLE: string;
@@ -40,3 +42,19 @@ export interface ICombinedBooksAndHighlights {
bookCoverUrl: string;
annotations: IHighlight[];
}
+
+export enum IHighlightsSortingCriterion {
+ CreationDateOldToNew = 'creationDateOldToNew',
+ CreationDateNewToOld = 'creationDateNewToOld',
+ LastModifiedDateOldToNew = 'lastModifiedDateOldToNew',
+ LastModifiedDateNewToOld = 'lastModifiedDateNewToOld',
+ Book = 'book'
+}
+
+export interface IBookHighlightsPluginSettings {
+ highlightsFolder: string;
+ backup: boolean;
+ importOnStart: boolean;
+ highlightsSortingCriterion: IHighlightsSortingCriterion;
+ template: Template;
+}
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 7ba8ad1..11108f0 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -3,8 +3,22 @@ import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import Handlebars from 'handlebars';
+export const calculateAppleDate = (date: number) => {
+ dayjs.extend(utc);
+ dayjs.extend(timezone);
+
+ const timeZone = dayjs.tz.guess();
+
+ const APPLE_EPOCH_START = new Date('2001-01-01').getTime();
+ const dateInMilliseconds = date * 1000;
+ const calculatedDate = dayjs(APPLE_EPOCH_START)
+ .add(dateInMilliseconds, 'ms')
+ .tz(timeZone || 'UTC');
+
+ return calculatedDate;
+}
+
(() => {
-// TODO: Consider moving to a separate file if there are several helpers to be added
Handlebars.registerHelper('eq', function (a, b) {
if (a == b) {
return this;
@@ -13,17 +27,7 @@ import Handlebars from 'handlebars';
// TODO: Consider using out-of-the-box date validation via https://day.js.org/docs/en/parse/is-valid
Handlebars.registerHelper('dateFormat', (date, format) => {
- dayjs.extend(utc);
- dayjs.extend(timezone);
-
- const timeZone = dayjs.tz.guess();
-
- const APPLE_EPOCH_START = new Date('2001-01-01').getTime();
- const dateInMilliseconds = date * 1000;
- const calculatedDate = dayjs(APPLE_EPOCH_START)
- .add(dateInMilliseconds, 'ms')
- .tz(timeZone || 'UTC');
-
+ const calculatedDate = calculateAppleDate(date);
const formattedDate = calculatedDate.format(format);
return formattedDate;
diff --git a/test/aggregateDetails.spec.ts b/test/aggregateDetails.spec.ts
index f0c9ab0..9a20598 100644
--- a/test/aggregateDetails.spec.ts
+++ b/test/aggregateDetails.spec.ts
@@ -1,17 +1,17 @@
import { describe, expect, test, vi } from 'vitest';
import { aggregateBookAndHighlightDetails } from '../src/methods/aggregateDetails';
import * as db from '../src/db';
-import { booksToAggregate, annotationsToAggregate, aggregatedHighlights } from './mocks/aggregatedDetailsData';
+import { booksToAggregate, annotationsToAggregate, aggregatedUnsortedHighlights } from './mocks/aggregatedDetailsData';
import { IBook, IBookAnnotation } from '../src/types';
describe('aggregateBookAndHighlightDetails', () => {
- test('Should return an array of aggregated highlights when a book has highlights', async () => {
+ test('Should return an array of aggregated unsorted highlights when a book has highlights', async () => {
vi.spyOn(db, 'dbRequest').mockResolvedValue(booksToAggregate as IBook[]);
vi.spyOn(db, 'annotationsRequest').mockResolvedValue(annotationsToAggregate as IBookAnnotation[]);
const books = await aggregateBookAndHighlightDetails();
- expect(books).toEqual(aggregatedHighlights);
+ expect(books).toEqual(aggregatedUnsortedHighlights);
});
test('Should return an empty array when a book has no highlights', async () => {
diff --git a/test/mocks/aggregatedDetailsData.ts b/test/mocks/aggregatedDetailsData.ts
index 9045c7b..caecc39 100644
--- a/test/mocks/aggregatedDetailsData.ts
+++ b/test/mocks/aggregatedDetailsData.ts
@@ -4,59 +4,133 @@ export const booksToAggregate = [{
"ZAUTHOR": "Apple Inc.",
"ZGENRE": "Technology",
"ZLANGUAGE": "EN",
- "ZLASTOPENDATE": 731876693.002279,
+ "ZLASTOPENDATE": 743629954.550869,
"ZCOVERURL": ''
}];
-export const annotationsToAggregate = [{
- "ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE5",
- "ZFUTUREPROOFING5": "Aggregated Introduction",
- "ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the aggregated highlight from the Apple iPhone User Guide",
- "ZANNOTATIONSELECTEDTEXT": "aggregated highlight from the Apple iPhone User Guide",
- "ZANNOTATIONLOCATION": "aggregated-highlight-link-from-the-apple-iphone-user-guide",
- "ZANNOTATIONNOTE": "Test note for the aggregated highlight from the Apple iPhone User Guide",
- "ZANNOTATIONCREATIONDATE": 731876693.002279,
- "ZANNOTATIONMODIFICATIONDATE": 731876693.002279,
- "ZANNOTATIONSTYLE": 3,
- "ZANNOTATIONDELETED": 0
-}, {
- "ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE5",
- "ZFUTUREPROOFING5": "Another aggregated Introduction",
- "ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the aggregated highlight from the Apple iPhone User Guide\n\ncontaining a new line to test the preservation of indentation",
- "ZANNOTATIONSELECTEDTEXT": "aggregated highlight from the Apple iPhone User Guide\n\ncontaining a new line to test the preservation of indentation\n\nand another new line\n\nto check one more time",
- "ZANNOTATIONLOCATION": "aggregated-highlight-link-from-the-apple-iphone-user-guide",
- "ZANNOTATIONNOTE": "Test note for the aggregated highlight from the Apple iPhone User Guide\n\nalong with a new line to test the preservation of indentation",
- "ZANNOTATIONCREATIONDATE": 731876693.002279,
- "ZANNOTATIONMODIFICATIONDATE": 731876693.002279,
- "ZANNOTATIONSTYLE": 3,
- "ZANNOTATIONDELETED": 0
+export const annotationsToAggregate = [
+ {
+ "ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE5",
+ "ZFUTUREPROOFING5": "Aggregated Introduction 3",
+ "ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the third aggregated highlight from the Apple iPhone User Guide",
+ "ZANNOTATIONSELECTEDTEXT": "third aggregated highlight from the Apple iPhone User Guide",
+ "ZANNOTATIONLOCATION": "epubcfi(/6/24[ch3]!/4/2/10,/1:19,/3:113)",
+ "ZANNOTATIONNOTE": "Test note for the third aggregated highlight from the Apple iPhone User Guide",
+ "ZANNOTATIONCREATIONDATE": 743629925.898202,
+ "ZANNOTATIONMODIFICATIONDATE": 743640744.124985,
+ "ZANNOTATIONSTYLE": 3,
+ "ZANNOTATIONDELETED": 0
+ },
+ {
+ "ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE5",
+ "ZFUTUREPROOFING5": "Aggregated Introduction 1",
+ "ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the first aggregated highlight from the Apple iPhone User Guide",
+ "ZANNOTATIONSELECTEDTEXT": "first aggregated highlight from the Apple iPhone User Guide",
+ "ZANNOTATIONLOCATION": "epubcfi(/6/12[ch1]!/4/2/10,/1:0,/:87)",
+ "ZANNOTATIONNOTE": "Test note for the first aggregated highlight from the Apple iPhone User Guide",
+ "ZANNOTATIONCREATIONDATE": 743629954.550827,
+ "ZANNOTATIONMODIFICATIONDATE": 743629954.550869,
+ "ZANNOTATIONSTYLE": 3,
+ "ZANNOTATIONDELETED": 0
+ },
+ {
+ "ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE5",
+ "ZFUTUREPROOFING5": "Aggregated Introduction 4",
+ "ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the fourth aggregated highlight from the Apple iPhone User Guide",
+ "ZANNOTATIONSELECTEDTEXT": "fourth aggregated highlight from the Apple iPhone User Guide",
+ "ZANNOTATIONLOCATION": "epubcfi(/6/36[ch4]!/10/2/4,/1:0,/:96)",
+ "ZANNOTATIONNOTE": "Test note for the fourth aggregated highlight from the Apple iPhone User Guide",
+ "ZANNOTATIONCREATIONDATE": 743629949.224146,
+ "ZANNOTATIONMODIFICATIONDATE": 743629949.224197,
+ "ZANNOTATIONSTYLE": 3,
+ "ZANNOTATIONDELETED": 0
+ },
+ {
+ "ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE5",
+ "ZFUTUREPROOFING5": "Aggregated Introduction 2",
+ "ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the second aggregated highlight from the Apple iPhone User Guide\n\ncontaining a new line to test the preservation of indentation",
+ "ZANNOTATIONSELECTEDTEXT": "second aggregated highlight from the Apple iPhone User Guide\n\ncontaining a new line to test the preservation of indentation\n\nand another new line\n\nto check one more time",
+ "ZANNOTATIONLOCATION": "epubcfi(/6/18[ch2]!/4/2/10,/4/1:19,/3:113)",
+ "ZANNOTATIONNOTE": "Test note for the second aggregated highlight from the Apple iPhone User Guide\n\nalong with a new line to test the preservation of indentation",
+ "ZANNOTATIONCREATIONDATE": 743629937.38764,
+ "ZANNOTATIONMODIFICATIONDATE": 743640715.281904,
+ "ZANNOTATIONSTYLE": 3,
+ "ZANNOTATIONDELETED": 0
+ },
+];
+
+export const annotationThree = {
+ "chapter": "Aggregated Introduction 3",
+ "contextualText": "This is a contextual text for the third aggregated highlight from the Apple iPhone User Guide",
+ "highlight": "third aggregated highlight from the Apple iPhone User Guide",
+ "note": "Test note for the third aggregated highlight from the Apple iPhone User Guide",
+ "highlightLocation": "epubcfi(/6/24[ch3]!/4/2/10,/1:19,/3:113)",
+ "highlightStyle": 3,
+ "highlightCreationDate": 743629925.898202,
+ "highlightModificationDate": 743640744.124985
+};
+
+export const annotationOne = {
+ "chapter": "Aggregated Introduction 1",
+ "contextualText": "This is a contextual text for the first aggregated highlight from the Apple iPhone User Guide",
+ "highlight": "first aggregated highlight from the Apple iPhone User Guide",
+ "note": "Test note for the first aggregated highlight from the Apple iPhone User Guide",
+ "highlightLocation": "epubcfi(/6/12[ch1]!/4/2/10,/1:0,/:87)",
+ "highlightStyle": 3,
+ "highlightCreationDate": 743629954.550827,
+ "highlightModificationDate": 743629954.550869
+};
+
+export const annotationFour = {
+ "chapter": "Aggregated Introduction 4",
+ "contextualText": "This is a contextual text for the fourth aggregated highlight from the Apple iPhone User Guide",
+ "highlight": "fourth aggregated highlight from the Apple iPhone User Guide",
+ "note": "Test note for the fourth aggregated highlight from the Apple iPhone User Guide",
+ "highlightLocation": "epubcfi(/6/36[ch4]!/10/2/4,/1:0,/:96)",
+ "highlightStyle": 3,
+ "highlightCreationDate": 743629949.224146,
+ "highlightModificationDate": 743629949.224197
+};
+
+export const annotationTwo = {
+ "chapter": "Aggregated Introduction 2",
+ "contextualText": "This is a contextual text for the second aggregated highlight from the Apple iPhone User Guide\ncontaining a new line to test the preservation of indentation",
+ "highlight": "second aggregated highlight from the Apple iPhone User Guide\ncontaining a new line to test the preservation of indentation\nand another new line\nto check one more time",
+ "note": "Test note for the second aggregated highlight from the Apple iPhone User Guide\nalong with a new line to test the preservation of indentation",
+ "highlightLocation": "epubcfi(/6/18[ch2]!/4/2/10,/4/1:19,/3:113)",
+ "highlightStyle": 3,
+ "highlightCreationDate": 743629937.38764,
+ "highlightModificationDate": 743640715.281904
+};
+
+export const aggregatedUnsortedHighlights = [{
+ "bookTitle": "Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title",
+ "bookId": "THBFYNJKTGFTTVCGSAE5",
+ "bookAuthor": "Apple Inc.",
+ "bookGenre": "Technology",
+ "bookLanguage": "EN",
+ "bookLastOpenedDate": 743629954.550869,
+ "bookCoverUrl": '',
+ "annotations": [
+ annotationThree,
+ annotationOne,
+ annotationFour,
+ annotationTwo,
+ ]
}];
-export const aggregatedHighlights = [{
+export const aggregatedHighlightsWithDefaultSorting = [{
"bookTitle": "Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title",
"bookId": "THBFYNJKTGFTTVCGSAE5",
"bookAuthor": "Apple Inc.",
"bookGenre": "Technology",
"bookLanguage": "EN",
- "bookLastOpenedDate": 731876693.002279,
+ "bookLastOpenedDate": 743629954.550869,
"bookCoverUrl": '',
- "annotations": [{
- "chapter": "Aggregated Introduction",
- "contextualText": "This is a contextual text for the aggregated highlight from the Apple iPhone User Guide",
- "highlight": "aggregated highlight from the Apple iPhone User Guide",
- "note": "Test note for the aggregated highlight from the Apple iPhone User Guide",
- "highlightLocation": "aggregated-highlight-link-from-the-apple-iphone-user-guide",
- "highlightStyle": 3,
- "highlightCreationDate": 731876693.002279,
- "highlightModificationDate": 731876693.002279
- }, {
- "chapter": "Another aggregated Introduction",
- "contextualText": "This is a contextual text for the aggregated highlight from the Apple iPhone User Guide\ncontaining a new line to test the preservation of indentation",
- "highlight": "aggregated highlight from the Apple iPhone User Guide\ncontaining a new line to test the preservation of indentation\nand another new line\nto check one more time",
- "note": "Test note for the aggregated highlight from the Apple iPhone User Guide\nalong with a new line to test the preservation of indentation",
- "highlightLocation": "aggregated-highlight-link-from-the-apple-iphone-user-guide",
- "highlightStyle": 3,
- "highlightCreationDate": 731876693.002279,
- "highlightModificationDate": 731876693.002279
- }]
+ "annotations": [
+ annotationThree,
+ annotationTwo,
+ annotationFour,
+ annotationOne,
+ ]
}];
diff --git a/test/mocks/renderedTemplate.ts b/test/mocks/renderedTemplate.ts
index 5e5e78f..e6410d5 100644
--- a/test/mocks/renderedTemplate.ts
+++ b/test/mocks/renderedTemplate.ts
@@ -1,71 +1,121 @@
-export const defaultTemplateMock = `Title:: π Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title
+const renderedAnnotationThree = `- π Chapter:: Aggregated Introduction 3
+- π Context:: This is a contextual text for the third aggregated highlight from the Apple iPhone User Guide
+- π― Highlight:: third aggregated highlight from the Apple iPhone User Guide
+- π Note:: Test note for the third aggregated highlight from the Apple iPhone User Guide
+- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#epubcfi(/6/24[ch3]!/4/2/10,/1:19,/3:113))`;
+
+const renderedAnnotationOne = `- π Chapter:: Aggregated Introduction 1
+- π Context:: This is a contextual text for the first aggregated highlight from the Apple iPhone User Guide
+- π― Highlight:: first aggregated highlight from the Apple iPhone User Guide
+- π Note:: Test note for the first aggregated highlight from the Apple iPhone User Guide
+- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#epubcfi(/6/12[ch1]!/4/2/10,/1:0,/:87))`;
+
+const renderedAnnotationFour = `- π Chapter:: Aggregated Introduction 4
+- π Context:: This is a contextual text for the fourth aggregated highlight from the Apple iPhone User Guide
+- π― Highlight:: fourth aggregated highlight from the Apple iPhone User Guide
+- π Note:: Test note for the fourth aggregated highlight from the Apple iPhone User Guide
+- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#epubcfi(/6/36[ch4]!/10/2/4,/1:0,/:96))`;
+
+const renderedAnnotationTwo = `- π Chapter:: Aggregated Introduction 2
+- π Context:: This is a contextual text for the second aggregated highlight from the Apple iPhone User Guide\ncontaining a new line to test the preservation of indentation
+- π― Highlight:: second aggregated highlight from the Apple iPhone User Guide
+containing a new line to test the preservation of indentation
+and another new line
+to check one more time
+- π Note:: Test note for the second aggregated highlight from the Apple iPhone User Guide
+along with a new line to test the preservation of indentation
+- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#epubcfi(/6/18[ch2]!/4/2/10,/4/1:19,/3:113))`;
+
+export const defaultTemplateMockWithAnnotationsSortedByDefault = `Title:: π Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title
Author:: Apple Inc.
Link:: [Apple Books Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5)
## Annotations
-Number of annotations:: 2
+Number of annotations:: 4
----
-- π Chapter:: Aggregated Introduction
-- π Context:: This is a contextual text for the aggregated highlight from the Apple iPhone User Guide
-- π― Highlight:: aggregated highlight from the Apple iPhone User Guide
-- π Note:: Test note for the aggregated highlight from the Apple iPhone User Guide
-- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#aggregated-highlight-link-from-the-apple-iphone-user-guide)
+${renderedAnnotationThree}
----
-- π Chapter:: Another aggregated Introduction
-- π Context:: This is a contextual text for the aggregated highlight from the Apple iPhone User Guide
-containing a new line to test the preservation of indentation
-- π― Highlight:: aggregated highlight from the Apple iPhone User Guide
+${renderedAnnotationTwo}
+
+----
+
+${renderedAnnotationFour}
+
+----
+
+${renderedAnnotationOne}
+
+`;
+
+const renderedColoredAnnotationThree = `- π Chapter:: Aggregated Introduction 3
+- π Context:: This is a contextual text for the third aggregated highlight from the Apple iPhone User Guide
+- π― Highlight:: third aggregated highlight from the Apple iPhone User Guide
+- π Note:: Test note for the third aggregated highlight from the Apple iPhone User Guide
+- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#epubcfi(/6/24[ch3]!/4/2/10,/1:19,/3:113))
+- π
Highlight taken on:: 2024-07-25 03:52:05 PM -04:00
+- π
Highlight modified on:: 2024-07-25 06:52:24 PM -04:00`;
+
+const renderedColoredAnnotationOne = `- π Chapter:: Aggregated Introduction 1
+- π Context:: This is a contextual text for the first aggregated highlight from the Apple iPhone User Guide
+- π― Highlight:: first aggregated highlight from the Apple iPhone User Guide
+- π Note:: Test note for the first aggregated highlight from the Apple iPhone User Guide
+- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#epubcfi(/6/12[ch1]!/4/2/10,/1:0,/:87))
+- π
Highlight taken on:: 2024-07-25 03:52:34 PM -04:00
+- π
Highlight modified on:: 2024-07-25 03:52:34 PM -04:00`;
+
+const renderedColoredAnnotationFour = `- π Chapter:: Aggregated Introduction 4
+- π Context:: This is a contextual text for the fourth aggregated highlight from the Apple iPhone User Guide
+- π― Highlight:: fourth aggregated highlight from the Apple iPhone User Guide
+- π Note:: Test note for the fourth aggregated highlight from the Apple iPhone User Guide
+- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#epubcfi(/6/36[ch4]!/10/2/4,/1:0,/:96))
+- π
Highlight taken on:: 2024-07-25 03:52:29 PM -04:00
+- π
Highlight modified on:: 2024-07-25 03:52:29 PM -04:00`;
+
+const renderedColoredAnnotationTwo = `- π Chapter:: Aggregated Introduction 2
+- π Context:: This is a contextual text for the second aggregated highlight from the Apple iPhone User Guide\ncontaining a new line to test the preservation of indentation
+- π― Highlight:: second aggregated highlight from the Apple iPhone User Guide
containing a new line to test the preservation of indentation
and another new line
-to check one more time
-- π Note:: Test note for the aggregated highlight from the Apple iPhone User Guide
+to check one more time
+- π Note:: Test note for the second aggregated highlight from the Apple iPhone User Guide
along with a new line to test the preservation of indentation
-- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#aggregated-highlight-link-from-the-apple-iphone-user-guide)
-
-`;
+- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#epubcfi(/6/18[ch2]!/4/2/10,/4/1:19,/3:113))
+- π
Highlight taken on:: 2024-07-25 03:52:17 PM -04:00
+- π
Highlight modified on:: 2024-07-25 06:51:55 PM -04:00`;
-export const renderedCustomTemplateMock = `Title:: π Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title
+export const renderedCustomTemplateMockWithDefaultSorting = `Title:: π Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title
Author:: Apple Inc.
Genre:: Technology
Language:: EN
-Last Read:: 2024-03-11 03:04:53 PM -04:00
+Last Read:: 2024-07-25 03:52:34 PM -04:00
Link:: [Apple Books Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5)
## Annotations
-Number of annotations:: 2
+Number of annotations:: 4
----
-- π Chapter:: Aggregated Introduction
-- π Context:: This is a contextual text for the aggregated highlight from the Apple iPhone User Guide
-- π― Highlight:: aggregated highlight from the Apple iPhone User Guide
-- π Note:: Test note for the aggregated highlight from the Apple iPhone User Guide
-- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#aggregated-highlight-link-from-the-apple-iphone-user-guide)
-- π
Highlight taken on:: 2024-03-11 03:04:53 PM -04:00
-- π
Highlight modified on:: 2024-03-11 03:04:53 PM -04:00
+${renderedColoredAnnotationThree}
----
-- π Chapter:: Another aggregated Introduction
-- π Context:: This is a contextual text for the aggregated highlight from the Apple iPhone User Guide
-containing a new line to test the preservation of indentation
-- π― Highlight:: aggregated highlight from the Apple iPhone User Guide
-containing a new line to test the preservation of indentation
-and another new line
-to check one more time
-- π Note:: Test note for the aggregated highlight from the Apple iPhone User Guide
-along with a new line to test the preservation of indentation
-- π Highlight Link:: [Apple Books Highlight Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5#aggregated-highlight-link-from-the-apple-iphone-user-guide)
-- π
Highlight taken on:: 2024-03-11 03:04:53 PM -04:00
-- π
Highlight modified on:: 2024-03-11 03:04:53 PM -04:00
+${renderedColoredAnnotationTwo}
+
+----
+
+${renderedColoredAnnotationFour}
+
+----
+
+${renderedColoredAnnotationOne}
`;
@@ -75,19 +125,29 @@ Link:: [Apple Books Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5)
## Annotations
-Number of annotations:: 2
+Number of annotations:: 4
----
> [!QUOTE]
-> aggregated highlight from the Apple iPhone User Guide
+> third aggregated highlight from the Apple iPhone User Guide
----
> [!QUOTE]
-> aggregated highlight from the Apple iPhone User Guide
+> second aggregated highlight from the Apple iPhone User Guide
containing a new line to test the preservation of indentation
and another new line
to check one more time
+----
+
+> [!QUOTE]
+> fourth aggregated highlight from the Apple iPhone User Guide
+
+----
+
+> [!QUOTE]
+> first aggregated highlight from the Apple iPhone User Guide
+
`;
diff --git a/test/renderHighlightsTemplate.spec.ts b/test/renderHighlightsTemplate.spec.ts
index bbe0130..fea0189 100644
--- a/test/renderHighlightsTemplate.spec.ts
+++ b/test/renderHighlightsTemplate.spec.ts
@@ -4,9 +4,9 @@ import timezone from 'dayjs/plugin/timezone';
import Handlebars from 'handlebars';
import { describe, expect, test, vi } from 'vitest';
import { renderHighlightsTemplate } from '../src/methods/renderHighlightsTemplate';
-import { aggregatedHighlights } from './mocks/aggregatedDetailsData';
+import { aggregatedHighlightsWithDefaultSorting } from './mocks/aggregatedDetailsData';
import { rawCustomTemplateMock, rawCustomTemplateMockWithWrappedTextBlockContainingNewlines } from './mocks/rawTemplates';
-import { defaultTemplateMock, renderedCustomTemplateMock, renderedCustomTemplateMockWithWrappedTextBlockContainingNewlines } from './mocks/renderedTemplate';
+import { defaultTemplateMockWithAnnotationsSortedByDefault, renderedCustomTemplateMockWithDefaultSorting, renderedCustomTemplateMockWithWrappedTextBlockContainingNewlines } from './mocks/renderedTemplate';
import defaultTemplate from '../src/template';
import { ICombinedBooksAndHighlights } from 'src/types';
@@ -19,21 +19,21 @@ describe('renderHighlightsTemplate', () => {
describe('Template rendering', () => {
test('Should render a default template with the provided data', async () => {
- const renderedTemplate = await renderHighlightsTemplate(aggregatedHighlights[0] as ICombinedBooksAndHighlights, defaultTemplate);
+ const renderedTemplate = await renderHighlightsTemplate(aggregatedHighlightsWithDefaultSorting[0] as ICombinedBooksAndHighlights, defaultTemplate);
- expect(renderedTemplate).toEqual(defaultTemplateMock);
+ expect(renderedTemplate).toEqual(defaultTemplateMockWithAnnotationsSortedByDefault);
});
test('Should render a custom template with the provided data', async () => {
tzSpy.mockImplementation(() => 'America/New_York');
- const renderedTemplate = await renderHighlightsTemplate(aggregatedHighlights[0] as ICombinedBooksAndHighlights, rawCustomTemplateMock);
+ const renderedTemplate = await renderHighlightsTemplate(aggregatedHighlightsWithDefaultSorting[0] as ICombinedBooksAndHighlights, rawCustomTemplateMock);
- expect(renderedTemplate).toEqual(renderedCustomTemplateMock);
+ expect(renderedTemplate).toEqual(renderedCustomTemplateMockWithDefaultSorting);
});
test('Should render a custom template with the provided data and preserve newlines in wrapped text blocks', async () => {
- const renderedTemplate = await renderHighlightsTemplate(aggregatedHighlights[0] as ICombinedBooksAndHighlights, rawCustomTemplateMockWithWrappedTextBlockContainingNewlines);
+ const renderedTemplate = await renderHighlightsTemplate(aggregatedHighlightsWithDefaultSorting[0] as ICombinedBooksAndHighlights, rawCustomTemplateMockWithWrappedTextBlockContainingNewlines);
expect(renderedTemplate).toEqual(renderedCustomTemplateMockWithWrappedTextBlockContainingNewlines);
});
diff --git a/test/saveHighlightsToVault.spec.ts b/test/saveHighlightsToVault.spec.ts
index 6862ce8..9e0c8d1 100644
--- a/test/saveHighlightsToVault.spec.ts
+++ b/test/saveHighlightsToVault.spec.ts
@@ -5,10 +5,10 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import SaveHighlights from '../src/methods/saveHighlightsToVault';
import { AppleBooksHighlightsImportPluginSettings } from '../src/settings';
import { rawCustomTemplateMock, rawCustomTemplateMockWithWrappedTextBlockContainingNewlines } from './mocks/rawTemplates';
-import { aggregatedHighlights } from './mocks/aggregatedDetailsData';
+import { aggregatedUnsortedHighlights } from './mocks/aggregatedDetailsData';
import {
- defaultTemplateMock,
- renderedCustomTemplateMock,
+ defaultTemplateMockWithAnnotationsSortedByDefault,
+ renderedCustomTemplateMockWithDefaultSorting,
renderedCustomTemplateMockWithWrappedTextBlockContainingNewlines,
} from './mocks/renderedTemplate';
import { ICombinedBooksAndHighlights } from '../src/types'
@@ -56,7 +56,7 @@ describe('Save highlights to vault', () => {
const saveHighlights = new SaveHighlights({ vault: mockVault } as any, settings);
const spyGetAbstractFileByPath = vi.spyOn(mockVault, 'getAbstractFileByPath').mockReturnValue('ibooks-highlights');
- await saveHighlights.saveHighlightsToVault(aggregatedHighlights as ICombinedBooksAndHighlights[]);
+ await saveHighlights.saveHighlightsToVault(aggregatedUnsortedHighlights as ICombinedBooksAndHighlights[]);
expect(spyGetAbstractFileByPath).toHaveBeenCalledTimes(1);
expect(spyGetAbstractFileByPath).toHaveBeenCalledWith('ibooks-highlights');
@@ -70,7 +70,7 @@ describe('Save highlights to vault', () => {
expect(mockVault.create).toHaveBeenCalledTimes(1);
expect(mockVault.create).toHaveBeenCalledWith(
`ibooks-highlights/Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title.md`,
- defaultTemplateMock
+ defaultTemplateMockWithAnnotationsSortedByDefault
);
});
@@ -83,7 +83,7 @@ describe('Save highlights to vault', () => {
const saveHighlights = new SaveHighlights({ vault: mockVault } as any, settings);
const spyGetAbstractFileByPath = vi.spyOn(mockVault, 'getAbstractFileByPath').mockReturnValue('ibooks-highlights');
- await saveHighlights.saveHighlightsToVault(aggregatedHighlights as ICombinedBooksAndHighlights[]);
+ await saveHighlights.saveHighlightsToVault(aggregatedUnsortedHighlights as ICombinedBooksAndHighlights[]);
expect(spyGetAbstractFileByPath).toHaveBeenCalledTimes(1);
expect(spyGetAbstractFileByPath).toHaveBeenCalledWith('ibooks-highlights');
@@ -97,7 +97,7 @@ describe('Save highlights to vault', () => {
expect(mockVault.create).toHaveBeenCalledTimes(1);
expect(mockVault.create).toHaveBeenCalledWith(
`ibooks-highlights/Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title.md`,
- renderedCustomTemplateMock
+ renderedCustomTemplateMockWithDefaultSorting
);
});
@@ -107,7 +107,7 @@ describe('Save highlights to vault', () => {
const saveHighlights = new SaveHighlights({ vault: mockVault } as any, settings);
const spyGetAbstractFileByPath = vi.spyOn(mockVault, 'getAbstractFileByPath').mockReturnValue('ibooks-highlights');
- await saveHighlights.saveHighlightsToVault(aggregatedHighlights as ICombinedBooksAndHighlights[]);
+ await saveHighlights.saveHighlightsToVault(aggregatedUnsortedHighlights as ICombinedBooksAndHighlights[]);
expect(spyGetAbstractFileByPath).toHaveBeenCalledTimes(1);
expect(spyGetAbstractFileByPath).toHaveBeenCalledWith('ibooks-highlights');
@@ -130,7 +130,7 @@ describe('Save highlights to vault', () => {
const saveHighlights = new SaveHighlights({ vault: mockVault } as any, { ...settings, highlightsFolder: '' });
const spyGetAbstractFileByPath = vi.spyOn(mockVault, 'getAbstractFileByPath').mockReturnValue('');
- await saveHighlights.saveHighlightsToVault(aggregatedHighlights as ICombinedBooksAndHighlights[]);
+ await saveHighlights.saveHighlightsToVault(aggregatedUnsortedHighlights as ICombinedBooksAndHighlights[]);
expect(spyGetAbstractFileByPath).toHaveBeenCalledTimes(1);
expect(spyGetAbstractFileByPath).toHaveBeenCalledWith('');
@@ -156,7 +156,7 @@ describe('Save highlights to vault', () => {
};
});
- await saveHighlights.saveHighlightsToVault(aggregatedHighlights as ICombinedBooksAndHighlights[]);
+ await saveHighlights.saveHighlightsToVault(aggregatedUnsortedHighlights as ICombinedBooksAndHighlights[]);
expect(spyList).toHaveBeenCalledTimes(1);
expect(spyList).toReturnWith({
diff --git a/test/settings.spec.ts b/test/settings.spec.ts
index d2a449e..ed2a9c7 100644
--- a/test/settings.spec.ts
+++ b/test/settings.spec.ts
@@ -16,6 +16,10 @@ describe('Plugin default settings', () => {
expect(settings.backup).toBeFalsy();
});
+ test("Highlights sorting criterion", () => {
+ expect(settings.highlightsSortingCriterion).toEqual('creationDateOldToNew');
+ });
+
test('Template', () => {
expect(settings.template).toEqual(defaultTemplate);
});
diff --git a/test/sortHighlights.spec.ts b/test/sortHighlights.spec.ts
new file mode 100644
index 0000000..42da36a
--- /dev/null
+++ b/test/sortHighlights.spec.ts
@@ -0,0 +1,179 @@
+import { describe, expect, test } from 'vitest';
+import { sortHighlights, highlightLocationToNumber, compareLocations } from '../src/methods/sortHighlights';
+import {
+ aggregatedUnsortedHighlights,
+ annotationOne,
+ annotationTwo,
+ annotationThree,
+ annotationFour
+} from './mocks/aggregatedDetailsData';
+import { ICombinedBooksAndHighlights, IHighlightsSortingCriterion} from '../src/types';
+
+describe('sortHighlights', () => {
+ describe('Highlights sorting criterion', () => {
+ test('Should sort highlights by creation date from oldest to newest', () => {
+ const highlightsSortingCriterion = IHighlightsSortingCriterion.CreationDateOldToNew;
+
+ const actual = sortHighlights(aggregatedUnsortedHighlights[0] as ICombinedBooksAndHighlights, highlightsSortingCriterion);
+
+ const expectedSortingForHighlights = {
+ ...aggregatedUnsortedHighlights[0],
+ annotations: [
+ annotationThree,
+ annotationTwo,
+ annotationFour,
+ annotationOne,
+ ]
+ };
+
+ expect(actual).toEqual(expectedSortingForHighlights);
+ });
+
+ test('Should sort highlights by creation date from newest to oldest', () => {
+ const highlightsSortingCriterion = IHighlightsSortingCriterion.CreationDateNewToOld;
+
+ const actual = sortHighlights(aggregatedUnsortedHighlights[0] as ICombinedBooksAndHighlights, highlightsSortingCriterion);
+
+ const expectedSortingForHighlights = {
+ ...aggregatedUnsortedHighlights[0],
+ annotations: [
+ annotationOne,
+ annotationFour,
+ annotationTwo,
+ annotationThree,
+ ]
+ };
+
+ expect(actual).toEqual(expectedSortingForHighlights);
+ });
+
+ test('Should sort highlights by modification date from oldest to newest', () => {
+ const highlightsSortingCriterion = IHighlightsSortingCriterion.LastModifiedDateOldToNew;
+
+ const actual = sortHighlights(aggregatedUnsortedHighlights[0] as ICombinedBooksAndHighlights, highlightsSortingCriterion);
+
+ const expectedSortingForHighlights = {
+ ...aggregatedUnsortedHighlights[0],
+ annotations: [
+ annotationFour,
+ annotationOne,
+ annotationTwo,
+ annotationThree,
+ ]
+ };
+
+ expect(actual).toEqual(expectedSortingForHighlights);
+ });
+
+ test('Should sort highlights by modification date from newest to oldest', () => {
+ const highlightsSortingCriterion = IHighlightsSortingCriterion.LastModifiedDateNewToOld;
+
+ const actual = sortHighlights(aggregatedUnsortedHighlights[0] as ICombinedBooksAndHighlights, highlightsSortingCriterion);
+
+ const expectedSortingForHighlights = {
+ ...aggregatedUnsortedHighlights[0],
+ annotations: [
+ annotationThree,
+ annotationTwo,
+ annotationOne,
+ annotationFour,
+ ]
+ };
+
+ expect(actual).toEqual(expectedSortingForHighlights);
+ });
+
+ test('Should sort highlights by location in a book', () => {
+ const highlightsSortingCriterion = IHighlightsSortingCriterion.Book;
+
+ const actual = sortHighlights(aggregatedUnsortedHighlights[0] as ICombinedBooksAndHighlights, highlightsSortingCriterion);
+
+ const expectedSortingForHighlights = {
+ ...aggregatedUnsortedHighlights[0],
+ annotations: [
+ annotationOne,
+ annotationTwo,
+ annotationThree,
+ annotationFour,
+ ]
+ };
+
+ expect(actual).toEqual(expectedSortingForHighlights);
+ });
+ });
+
+ describe('sortHighlights auxiliary functions', () => {
+ describe('highlightLocationToNumber', () => {
+ test('Should return the correct array of numbers from a highlight location', () => {
+ const highlightLocations = [
+ 'epubcfi(/6/2[iphd3c6e37c7]!/4[iphd3c6e37c7]/6[iphefb3daa42]/6[iph22e5dfab7]/4/2/1,:0,:28)',
+ 'epubcfi(/6/12[chapter1]!/4/2/16/1,:0,:87)',
+ 'epubcfi(/6/12[x06_Introduction_How]!/4[x9780593422984_EPUB-4]/2/82,/1:0,/5:98)',
+ 'epubcfi(/6/28[chapter-idp13097504]!/4/2/2[labeling_systems]/24/2[recap-id00006]/6/6/2/1,:0,:120)',
+ 'epubcfi(/6/34[data-uuid-42e193cde5544ad5ac31696d78c19bf9]!/4/16[data-uuid-59056f0b33a144929eb7810513ae7131],/1:0,/1:292)',
+ 'epubcfi(/6/34[chap10]!/4/2[chap10.html]/8/8,/2/1:0,/4/1:11)',
+ 'epubcfi(/6/36[ch10]!/4/2/10,/1:19,/3:113)',
+ 'epubcfi(/6/72[x9780735211308_EPUB-34]!/4[x9780735211308_EPUB-34]/2,/58/3:1,/60/1:53)',
+ 'epubcfi(/6/86[chapter00191]!/4/166/1,:10,:197)',
+ 'epubcfi(/6/198[ch15_sub01]!/4/2/3,:0,:96)'
+ ];
+
+ const expectedNumbers = [
+ [6, 2, 4, 6, 6, 4, 2, 1, 0],
+ [6, 12, 4, 2, 16, 1, 0],
+ [6, 12, 4, 2, 82, 1, 0],
+ [6, 28, 4, 2, 2, 24, 2, 6, 6, 2, 1, 0],
+ [6, 34, 4, 16, 1, 0],
+ [6, 34, 4, 2, 8, 8, 2, 1, 0],
+ [6, 36, 4, 2, 10, 1, 19],
+ [6, 72, 4, 2, 58, 3, 1],
+ [6, 86, 4, 166, 1, 10],
+ [6, 198, 4, 2, 3, 0],
+ ];
+
+ highlightLocations.forEach((highlightLocation, index) => {
+ const actual = highlightLocationToNumber(highlightLocation);
+ expect(actual).toEqual(expectedNumbers[index]);
+ });
+ });
+ });
+
+ describe('compareLocations', () => {
+ test('Should return -1 if the first location is less than the second one', () => {
+ const firstLocation = [6, 12, 4, 2, 16, 1, 0];
+ const secondLocation = [6, 12, 4, 2, 82, 1, 0];
+ const actual = compareLocations(firstLocation, secondLocation);
+ expect(actual).toBe(-1);
+ });
+
+ test('Should return 1 if the first location is greater than the second one', () => {
+ const firstLocation = [6, 12, 4, 2, 82, 1, 0];
+ const secondLocation = [6, 12, 4, 2, 16, 1, 0];
+ const actual = compareLocations(firstLocation, secondLocation);
+ expect(actual).toBe(1);
+ });
+
+ test('Should return the positive difference (+2) in lengths if the locations are equal up to the length of the shorter one (first location)', () => {
+ const firstLocation = [6, 34, 4, 16, 1, 10, 2, 0];
+ const secondLocation = [6, 34, 4, 16, 1, 10];
+ const actual = compareLocations(firstLocation, secondLocation);
+ expect(actual).toBe(2);
+ });
+
+ test('Should return the negative difference (-2) in lengths if the locations are equal up to the length of the shorter one (second location)', () => {
+ const firstLocation = [6, 34, 4, 16, 1, 10];
+ const secondLocation = [6, 34, 4, 16, 1, 10, 2, 0];
+ const actual = compareLocations(firstLocation, secondLocation);
+ expect(actual).toBe(-2);
+ });
+
+ test('Should return 0 if the locations are equal', () => {
+ const firstLocation = [6, 12, 4, 2, 16, 1, 0];
+ const secondLocation = [6, 12, 4, 2, 16, 1, 0];
+ const actual = compareLocations(firstLocation, secondLocation);
+ expect(actual).toBe(0);
+ });
+ });
+ })
+
+});