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/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); + }); + }); + }) + +});