• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bandantonio / obsidian-apple-books-highlights-plugin / 21190206742

18 Jan 2026 09:24PM UTC coverage: 71.753%. First build
21190206742

push

github

bandantonio
refactor: add debug timers

78 of 85 branches covered (91.76%)

Branch coverage included in aggregate %.

88 of 184 new or added lines in 8 files covered. (47.83%)

364 of 531 relevant lines covered (68.55%)

8.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.99
/src/services/highlightProcessingService.ts
1
import type { ICombinedBooksAndHighlights, IDataService, IHighlight, IHighlightProcessingService } from '../types';
1✔
2
import { IHighlightsSortingCriterion } from '../types';
3
import { preserveNewlineIndentation, removeTrailingSpaces } from '../utils';
4
import type { DiagnosticsCollector } from '../utils/diagnostics';
5
import { Timer } from '../utils/timing';
6

7
export class HighlightProcessingService implements IHighlightProcessingService {
1✔
8
  private dataService: IDataService;
9
  private diagnosticsCollector?: DiagnosticsCollector;
10

11
  constructor(dataService: IDataService, diagnosticsCollector?: DiagnosticsCollector) {
1✔
12
    this.dataService = dataService;
25✔
13
    this.diagnosticsCollector = diagnosticsCollector;
25✔
14
  }
25✔
15

16
  async aggregateHighlights(): Promise<ICombinedBooksAndHighlights[]> {
1✔
17
    const timer = new Timer('HighlightProcessingService.aggregateHighlights', this.diagnosticsCollector);
3✔
18
    timer.start();
3✔
19
    try {
3✔
20
      const books = await this.dataService.getBooks();
3✔
21
      const annotations = await this.dataService.getAnnotations();
3✔
22

23
      const resultingHighlights: ICombinedBooksAndHighlights[] = books.reduce((highlights: ICombinedBooksAndHighlights[], book) => {
3✔
24
        const bookRelatedAnnotations = annotations.filter((annotation) => annotation.ZANNOTATIONASSETID === book.ZASSETID);
3✔
25

26
        if (bookRelatedAnnotations.length > 0) {
3✔
27
          // Obsidian forbids adding certain characters to the title of a note, so they must be replaced with a dash (-)
28
          // | # ^ [] \ / :
29
          // after the replacement, two or more spaces are replaced with a single one
30
          const normalizedBookTitle = book.ZTITLE.replace(/[|#^[\]\\/:]+/g, ' -').replace(/\s{2,}/g, ' ');
2✔
31

32
          highlights.push({
2✔
33
            bookTitle: normalizedBookTitle,
2✔
34
            bookId: book.ZASSETID,
2✔
35
            bookAuthor: book.ZAUTHOR,
2✔
36
            bookGenre: book.ZGENRE,
2✔
37
            bookLanguage: book.ZLANGUAGE,
2✔
38
            bookLastOpenedDate: book.ZLASTOPENDATE,
2✔
39
            bookFinishedDate: book.ZDATEFINISHED,
2✔
40
            bookCoverUrl: book.ZCOVERURL,
2✔
41
            annotations: bookRelatedAnnotations.map((annotation) => {
2✔
42
              const textForContext = annotation.ZANNOTATIONREPRESENTATIVETEXT;
6✔
43
              const userNote = annotation.ZANNOTATIONNOTE;
6✔
44

45
              return {
6✔
46
                chapter: annotation.ZFUTUREPROOFING5,
6✔
47
                contextualText: removeTrailingSpaces(preserveNewlineIndentation(textForContext)),
6✔
48
                highlight: preserveNewlineIndentation(annotation.ZANNOTATIONSELECTEDTEXT),
6✔
49
                note: userNote ? preserveNewlineIndentation(userNote) : userNote,
6✔
50
                highlightLocation: annotation.ZANNOTATIONLOCATION,
6✔
51
                highlightStyle: annotation.ZANNOTATIONSTYLE,
6✔
52
                highlightCreationDate: annotation.ZANNOTATIONCREATIONDATE,
6✔
53
                highlightModificationDate: annotation.ZANNOTATIONMODIFICATIONDATE,
6✔
54
              };
6✔
55
            }),
2✔
56
          });
2✔
57
        }
2✔
58

59
        return highlights;
3✔
60
      }, []);
3✔
61
      timer.end();
3✔
62
      return resultingHighlights;
3✔
63
    } catch (error) {
3!
NEW
64
      timer.end();
×
65
      throw new Error(`Error aggregating highlights: ${(error as Error).message}`);
×
66
    }
×
67
  }
3✔
68

69
  sortHighlights(
1✔
70
    combinedHighlight: ICombinedBooksAndHighlights,
16✔
71
    highlightsSortingCriterion: IHighlightsSortingCriterion,
16✔
72
  ): ICombinedBooksAndHighlights {
16✔
73
    const timer = new Timer('HighlightProcessingService.sortHighlights', this.diagnosticsCollector);
16✔
74
    timer.start();
16✔
75
    let sortedHighlights: IHighlight[] = [];
16✔
76

77
    switch (highlightsSortingCriterion) {
16✔
78
      case IHighlightsSortingCriterion.CreationDateOldToNew:
16✔
79
        sortedHighlights = combinedHighlight.annotations.sort((a, b) => a.highlightCreationDate - b.highlightCreationDate);
12✔
80
        break;
12✔
81
      case IHighlightsSortingCriterion.CreationDateNewToOld:
16✔
82
        sortedHighlights = combinedHighlight.annotations.sort((a, b) => b.highlightCreationDate - a.highlightCreationDate);
1✔
83
        break;
1✔
84
      case IHighlightsSortingCriterion.LastModifiedDateOldToNew:
16✔
85
        sortedHighlights = combinedHighlight.annotations.sort((a, b) => a.highlightModificationDate - b.highlightModificationDate);
1✔
86
        break;
1✔
87
      case IHighlightsSortingCriterion.LastModifiedDateNewToOld:
16✔
88
        sortedHighlights = combinedHighlight.annotations.sort((a, b) => b.highlightModificationDate - a.highlightModificationDate);
1✔
89
        break;
1✔
90
      case IHighlightsSortingCriterion.Book:
16✔
91
        sortedHighlights = combinedHighlight.annotations.sort((a, b) => {
1✔
92
          const firstHighlightLocation = this.highlightLocationToNumber(a.highlightLocation);
5✔
93
          const secondHighlightLocation = this.highlightLocationToNumber(b.highlightLocation);
5✔
94

95
          return this.compareLocations(firstHighlightLocation, secondHighlightLocation);
5✔
96
        });
1✔
97
        break;
1✔
98
    }
16✔
99

100
    timer.end();
16✔
101
    return {
16✔
102
      ...combinedHighlight,
16✔
103
      annotations: sortedHighlights,
16✔
104
    };
16✔
105
  }
16✔
106

107
  // The function converts a highlight location string to an array of numbers
108
  // biome-ignore format: the current format is easier to read and understand
109
  highlightLocationToNumber(highlightLocation: string): number[] {
1✔
110
    // epubcfi example structure: epubcfi(/6/2[body01]!/4/2/2/1:0)
111
    return highlightLocation
20✔
112
      // biome-ignore lint/style/noMagicNumbers: 8 and -1 are positions of the epubcfi( and ) characters
113
      .slice(8, -1)        // Get rid of the epubcfi() wrapper
20✔
114
      .split(',')                // Split the locator into three parts: the common parent, the start subpath, and the end subpath
20✔
115
      .slice(0, -1)        // Get rid of the end subpath (third part)
20✔
116
      .join(',')                // Join the first two parts back together
20✔
117
      .match(/(?<!\[)[/:]\d+(?!\])/g)!         // Extract all the numbers (except those in square brackets) from the first two parts
20✔
118
      .map(match => parseInt(match.slice(1)))         // Get rid of the leading slash or colon and convert the string to a number
20✔
119
  }
20✔
120
  // biome-ignore format: the current format is easier to read and understand
121

122
  compareLocations = (firstLocation: number[], secondLocation: number[]) => {
1✔
123
    // Loop through each element of both arrays up to the length of the shorter one
124
    for (let i = 0; i < Math.min(firstLocation.length, secondLocation.length); i++) {
10✔
125
      if (firstLocation[i] < secondLocation[i]) {
39✔
126
        return -1;
3✔
127
      }
3✔
128
      if (firstLocation[i] > secondLocation[i]) {
39✔
129
        return 1;
4✔
130
      }
4✔
131
    }
39✔
132

133
    // If the loop didn't return, the arrays are equal up to the length of the shorter array
134
    // so the function returns the difference in lengths to determine the order of the corresponding locations
135
    return firstLocation.length - secondLocation.length;
3✔
136
  }
10✔
137
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc