apple-books-export 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,775 @@
1
+ function formatDate(date) {
2
+ return date.toLocaleDateString('en-US', {
3
+ year: 'numeric',
4
+ month: 'long',
5
+ day: 'numeric',
6
+ });
7
+ }
8
+ function escapeHtml(text) {
9
+ return text
10
+ .replace(/&/g, '&')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/'/g, '&#039;');
15
+ }
16
+ function getColorClass(color) {
17
+ const colorMap = {
18
+ yellow: 'yellow',
19
+ green: 'green',
20
+ blue: 'blue',
21
+ pink: 'pink',
22
+ purple: 'purple',
23
+ underline: 'underline',
24
+ };
25
+ return colorMap[color] || 'yellow';
26
+ }
27
+ function generateBookId(book) {
28
+ return `book-${book.assetId}`;
29
+ }
30
+ function renderAnnotation(annotation) {
31
+ const colorClass = getColorClass(annotation.color);
32
+ let html = ` <div class="highlight">\n`;
33
+ html += ` <span class="color-dot ${colorClass}"></span>\n`;
34
+ html += ` <div class="highlight-content">\n`;
35
+ if (annotation.text) {
36
+ html += ` <p class="highlight-text">${escapeHtml(annotation.text)}</p>\n`;
37
+ }
38
+ if (annotation.note) {
39
+ html += ` <p class="note">${escapeHtml(annotation.note)}</p>\n`;
40
+ }
41
+ html += ` <div class="highlight-meta">\n`;
42
+ if (annotation.location) {
43
+ html += ` <span class="location">${escapeHtml(annotation.location)}</span>\n`;
44
+ }
45
+ html += ` <time>${formatDate(annotation.createdAt)}</time>\n`;
46
+ html += ` </div>\n`;
47
+ html += ` </div>\n`;
48
+ html += ` </div>\n`;
49
+ return html;
50
+ }
51
+ function renderBook(book) {
52
+ const bookId = generateBookId(book);
53
+ const title = book.title || 'Unknown Title';
54
+ const author = book.author || 'Unknown Author';
55
+ let html = ` <section id="${bookId}" class="book">\n`;
56
+ html += ` <div class="book-header">\n`;
57
+ html += ` <div class="book-info">\n`;
58
+ html += ` <h2>${escapeHtml(title)}</h2>\n`;
59
+ html += ` <p class="author">by ${escapeHtml(author)}</p>\n`;
60
+ html += ` </div>\n`;
61
+ html += ` <button class="collapse-btn" aria-label="Toggle highlights">\n`;
62
+ html += ` <svg class="chevron" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n`;
63
+ html += ` <polyline points="6 9 12 15 18 9"></polyline>\n`;
64
+ html += ` </svg>\n`;
65
+ html += ` </button>\n`;
66
+ html += ` </div>\n`;
67
+ html += ` <div class="highlights">\n`;
68
+ for (const annotation of book.annotations) {
69
+ html += renderAnnotation(annotation);
70
+ }
71
+ html += ` </div>\n`;
72
+ html += ` </section>\n\n`;
73
+ return html;
74
+ }
75
+ function renderBookIndex(books) {
76
+ let html = ` <aside class="sidebar">\n`;
77
+ html += ` <h3>Library</h3>\n`;
78
+ html += ` <ul class="book-list">\n`;
79
+ for (const book of books) {
80
+ const bookId = generateBookId(book);
81
+ const title = book.title || 'Unknown Title';
82
+ const count = book.annotations.length;
83
+ html += ` <li>\n`;
84
+ html += ` <a href="#${bookId}" class="book-link">\n`;
85
+ html += ` <span class="book-title">${escapeHtml(title)}</span>\n`;
86
+ html += ` <span class="book-count">${count}</span>\n`;
87
+ html += ` </a>\n`;
88
+ html += ` </li>\n`;
89
+ }
90
+ html += ` </ul>\n`;
91
+ html += ` </aside>\n\n`;
92
+ return html;
93
+ }
94
+ export function exportToHtml(books, outputPath) {
95
+ const totalHighlights = books.reduce((sum, book) => sum + book.annotations.length, 0);
96
+ const totalBooks = books.length;
97
+ const exportDate = new Date().toLocaleDateString('en-US', {
98
+ year: 'numeric',
99
+ month: 'short',
100
+ day: 'numeric',
101
+ });
102
+ let html = `<!DOCTYPE html>
103
+ <html lang="en">
104
+ <head>
105
+ <meta charset="UTF-8">
106
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
107
+ <title>Apple Books Highlights</title>
108
+ <link rel="preconnect" href="https://fonts.googleapis.com">
109
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
110
+ <link href="https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
111
+ <style>
112
+ * {
113
+ box-sizing: border-box;
114
+ margin: 0;
115
+ padding: 0;
116
+ }
117
+
118
+ :root {
119
+ --bg-page: #faf8f5;
120
+ --bg-card: #ffffff;
121
+ --bg-sidebar: #f5f3f0;
122
+ --bg-hover: #f0ede8;
123
+ --text-primary: #2c2c2c;
124
+ --text-secondary: #6b6b6b;
125
+ --text-tertiary: #999999;
126
+ --accent-primary: #d4a574;
127
+ --border-color: #e8e5e0;
128
+ --dot-yellow: #ffc107;
129
+ --dot-green: #4caf50;
130
+ --dot-blue: #2196f3;
131
+ --dot-pink: #e91e63;
132
+ --dot-purple: #9c27b0;
133
+ --dot-underline: #757575;
134
+ }
135
+
136
+ body {
137
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
138
+ font-size: 1.0625rem;
139
+ line-height: 1.7;
140
+ color: var(--text-primary);
141
+ background: var(--bg-page);
142
+ margin: 0;
143
+ padding: 0;
144
+ }
145
+
146
+ /* Header */
147
+ .header {
148
+ position: sticky;
149
+ top: 0;
150
+ background: var(--bg-card);
151
+ border-bottom: 1px solid var(--border-color);
152
+ padding: 1.5rem 2rem;
153
+ z-index: 100;
154
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
155
+ }
156
+
157
+ .header-content {
158
+ display: flex;
159
+ justify-content: space-between;
160
+ align-items: center;
161
+ margin-bottom: 1rem;
162
+ }
163
+
164
+ .header h1 {
165
+ font-family: 'Crimson Text', Georgia, serif;
166
+ font-size: 2.5rem;
167
+ font-weight: 600;
168
+ color: var(--text-primary);
169
+ letter-spacing: -0.02em;
170
+ }
171
+
172
+ .header-actions {
173
+ display: flex;
174
+ gap: 0.75rem;
175
+ align-items: center;
176
+ }
177
+
178
+ .expand-all-btn {
179
+ padding: 0.5rem 1rem;
180
+ font-size: 0.9375rem;
181
+ font-family: inherit;
182
+ background: transparent;
183
+ border: 1px solid var(--border-color);
184
+ border-radius: 6px;
185
+ color: var(--text-secondary);
186
+ cursor: pointer;
187
+ transition: all 0.2s ease;
188
+ }
189
+
190
+ .expand-all-btn:hover {
191
+ background: var(--bg-hover);
192
+ border-color: var(--accent-primary);
193
+ color: var(--text-primary);
194
+ }
195
+
196
+ .sidebar-toggle {
197
+ display: none;
198
+ padding: 0.5rem;
199
+ font-size: 1.5rem;
200
+ background: transparent;
201
+ border: 1px solid var(--border-color);
202
+ border-radius: 6px;
203
+ color: var(--text-secondary);
204
+ cursor: pointer;
205
+ transition: all 0.2s ease;
206
+ }
207
+
208
+ .sidebar-toggle:hover {
209
+ background: var(--bg-hover);
210
+ color: var(--text-primary);
211
+ }
212
+
213
+ .search-container {
214
+ margin-bottom: 1rem;
215
+ }
216
+
217
+ .search-box {
218
+ width: 100%;
219
+ padding: 0.75rem 1rem;
220
+ font-size: 1rem;
221
+ font-family: inherit;
222
+ border: 1px solid var(--border-color);
223
+ border-radius: 8px;
224
+ background: var(--bg-page);
225
+ color: var(--text-primary);
226
+ transition: all 0.2s ease;
227
+ }
228
+
229
+ .search-box:focus {
230
+ outline: none;
231
+ border-color: var(--accent-primary);
232
+ box-shadow: 0 0 0 3px rgba(212, 165, 116, 0.1);
233
+ }
234
+
235
+ .stats {
236
+ color: var(--text-tertiary);
237
+ font-size: 0.9375rem;
238
+ }
239
+
240
+ /* Layout */
241
+ .container {
242
+ display: flex;
243
+ min-height: calc(100vh - 180px);
244
+ }
245
+
246
+ /* Sidebar */
247
+ .sidebar {
248
+ width: 280px;
249
+ background: var(--bg-sidebar);
250
+ border-right: 1px solid var(--border-color);
251
+ padding: 2rem 1.5rem;
252
+ overflow-y: auto;
253
+ position: sticky;
254
+ top: 180px;
255
+ height: calc(100vh - 180px);
256
+ flex-shrink: 0;
257
+ transition: transform 0.3s ease;
258
+ }
259
+
260
+ .sidebar.hidden {
261
+ transform: translateX(-100%);
262
+ position: absolute;
263
+ }
264
+
265
+ .sidebar h3 {
266
+ font-family: 'Crimson Text', Georgia, serif;
267
+ font-size: 1.125rem;
268
+ font-weight: 600;
269
+ margin-bottom: 1rem;
270
+ color: var(--text-primary);
271
+ }
272
+
273
+ .book-list {
274
+ list-style: none;
275
+ }
276
+
277
+ .book-list li {
278
+ margin-bottom: 0.5rem;
279
+ }
280
+
281
+ .book-link {
282
+ display: flex;
283
+ justify-content: space-between;
284
+ align-items: baseline;
285
+ padding: 0.5rem 0.75rem;
286
+ text-decoration: none;
287
+ color: var(--text-secondary);
288
+ border-radius: 6px;
289
+ transition: all 0.2s ease;
290
+ font-size: 0.9375rem;
291
+ }
292
+
293
+ .book-link:hover {
294
+ background: var(--bg-hover);
295
+ color: var(--accent-primary);
296
+ }
297
+
298
+ .book-link.active {
299
+ background: var(--bg-card);
300
+ color: var(--text-primary);
301
+ border-left: 3px solid var(--accent-primary);
302
+ }
303
+
304
+ .book-title {
305
+ flex: 1;
306
+ overflow: hidden;
307
+ text-overflow: ellipsis;
308
+ white-space: nowrap;
309
+ margin-right: 0.5rem;
310
+ }
311
+
312
+ .book-count {
313
+ font-size: 0.875rem;
314
+ color: var(--text-tertiary);
315
+ }
316
+
317
+ /* Main content */
318
+ main {
319
+ flex: 1;
320
+ padding: 3rem 2rem;
321
+ max-width: 900px;
322
+ margin: 0 auto;
323
+ }
324
+
325
+ /* Book cards */
326
+ .book {
327
+ background: var(--bg-card);
328
+ border-radius: 12px;
329
+ padding: 2rem;
330
+ margin-bottom: 2rem;
331
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
332
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
333
+ scroll-margin-top: 200px;
334
+ }
335
+
336
+ .book:hover {
337
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
338
+ }
339
+
340
+ .book-header {
341
+ display: flex;
342
+ justify-content: space-between;
343
+ align-items: flex-start;
344
+ margin-bottom: 1.5rem;
345
+ cursor: pointer;
346
+ user-select: none;
347
+ }
348
+
349
+ .book-info {
350
+ flex: 1;
351
+ }
352
+
353
+ .book h2 {
354
+ font-family: 'Crimson Text', Georgia, serif;
355
+ font-size: 1.75rem;
356
+ font-weight: 600;
357
+ color: var(--text-primary);
358
+ margin-bottom: 0.5rem;
359
+ line-height: 1.3;
360
+ }
361
+
362
+ .author {
363
+ color: var(--text-secondary);
364
+ font-size: 1rem;
365
+ }
366
+
367
+ .collapse-btn {
368
+ background: transparent;
369
+ border: none;
370
+ cursor: pointer;
371
+ padding: 0.5rem;
372
+ color: var(--text-secondary);
373
+ transition: all 0.2s ease;
374
+ border-radius: 6px;
375
+ }
376
+
377
+ .collapse-btn:hover {
378
+ background: var(--bg-hover);
379
+ color: var(--text-primary);
380
+ }
381
+
382
+ .chevron {
383
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
384
+ }
385
+
386
+ .book.collapsed .chevron {
387
+ transform: rotate(-90deg);
388
+ }
389
+
390
+ .highlights {
391
+ overflow: hidden;
392
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
393
+ }
394
+
395
+ .book.collapsed .highlights {
396
+ max-height: 0 !important;
397
+ margin-bottom: 0;
398
+ }
399
+
400
+ /* Highlight items */
401
+ .highlight {
402
+ display: flex;
403
+ gap: 12px;
404
+ margin-bottom: 1.5rem;
405
+ padding-bottom: 1.5rem;
406
+ border-bottom: 1px solid var(--border-color);
407
+ }
408
+
409
+ .highlight:last-child {
410
+ border-bottom: none;
411
+ padding-bottom: 0;
412
+ margin-bottom: 0;
413
+ }
414
+
415
+ .color-dot {
416
+ width: 8px;
417
+ height: 8px;
418
+ border-radius: 50%;
419
+ flex-shrink: 0;
420
+ margin-top: 6px;
421
+ }
422
+
423
+ .color-dot.yellow {
424
+ background: var(--dot-yellow);
425
+ box-shadow: 0 0 8px rgba(255, 193, 7, 0.3);
426
+ }
427
+
428
+ .color-dot.green {
429
+ background: var(--dot-green);
430
+ box-shadow: 0 0 8px rgba(76, 175, 80, 0.3);
431
+ }
432
+
433
+ .color-dot.blue {
434
+ background: var(--dot-blue);
435
+ box-shadow: 0 0 8px rgba(33, 150, 243, 0.3);
436
+ }
437
+
438
+ .color-dot.pink {
439
+ background: var(--dot-pink);
440
+ box-shadow: 0 0 8px rgba(233, 30, 99, 0.3);
441
+ }
442
+
443
+ .color-dot.purple {
444
+ background: var(--dot-purple);
445
+ box-shadow: 0 0 8px rgba(156, 39, 176, 0.3);
446
+ }
447
+
448
+ .color-dot.underline {
449
+ background: var(--dot-underline);
450
+ box-shadow: 0 0 8px rgba(117, 117, 117, 0.3);
451
+ }
452
+
453
+ .highlight-content {
454
+ flex: 1;
455
+ }
456
+
457
+ .highlight-text {
458
+ font-size: 1.0625rem;
459
+ line-height: 1.7;
460
+ color: var(--text-primary);
461
+ margin-bottom: 0.5rem;
462
+ }
463
+
464
+ .note {
465
+ font-style: italic;
466
+ color: var(--text-secondary);
467
+ margin-top: 0.75rem;
468
+ font-size: 1rem;
469
+ }
470
+
471
+ .highlight-meta {
472
+ display: flex;
473
+ justify-content: space-between;
474
+ align-items: center;
475
+ margin-top: 0.75rem;
476
+ gap: 1rem;
477
+ }
478
+
479
+ .location {
480
+ font-size: 0.875rem;
481
+ color: var(--text-tertiary);
482
+ }
483
+
484
+ time {
485
+ font-size: 0.875rem;
486
+ color: var(--text-tertiary);
487
+ text-align: right;
488
+ }
489
+
490
+ .hidden {
491
+ display: none;
492
+ }
493
+
494
+ /* Responsive design */
495
+ @media (max-width: 1023px) {
496
+ .sidebar {
497
+ position: fixed;
498
+ top: 0;
499
+ left: 0;
500
+ height: 100vh;
501
+ z-index: 200;
502
+ box-shadow: 2px 0 12px rgba(0, 0, 0, 0.1);
503
+ transform: translateX(-100%);
504
+ }
505
+
506
+ .sidebar.visible {
507
+ transform: translateX(0);
508
+ }
509
+
510
+ .sidebar-toggle {
511
+ display: block;
512
+ }
513
+
514
+ main {
515
+ padding: 2rem 1.5rem;
516
+ }
517
+
518
+ .book {
519
+ padding: 1.5rem;
520
+ }
521
+ }
522
+
523
+ @media (max-width: 767px) {
524
+ .header {
525
+ padding: 1rem 1.25rem;
526
+ }
527
+
528
+ .header h1 {
529
+ font-size: 1.75rem;
530
+ }
531
+
532
+ .expand-all-btn {
533
+ display: none;
534
+ }
535
+
536
+ main {
537
+ padding: 1.5rem 1rem;
538
+ }
539
+
540
+ .book {
541
+ padding: 1.25rem;
542
+ }
543
+
544
+ .book h2 {
545
+ font-size: 1.5rem;
546
+ }
547
+
548
+ .sidebar {
549
+ width: 260px;
550
+ }
551
+ }
552
+
553
+ /* Print styles */
554
+ @media print {
555
+ body {
556
+ background: white;
557
+ }
558
+
559
+ .header {
560
+ position: static;
561
+ box-shadow: none;
562
+ }
563
+
564
+ .search-container,
565
+ .expand-all-btn,
566
+ .sidebar-toggle,
567
+ .collapse-btn {
568
+ display: none !important;
569
+ }
570
+
571
+ .sidebar {
572
+ position: static;
573
+ width: 100%;
574
+ height: auto;
575
+ page-break-after: always;
576
+ }
577
+
578
+ .book {
579
+ box-shadow: none;
580
+ page-break-inside: avoid;
581
+ }
582
+
583
+ .book.collapsed .highlights {
584
+ max-height: none !important;
585
+ }
586
+
587
+ main {
588
+ padding: 1rem 0;
589
+ }
590
+ }
591
+ </style>
592
+ </head>
593
+ <body>
594
+ <header class="header">
595
+ <div class="header-content">
596
+ <h1>Apple Books Highlights</h1>
597
+ <div class="header-actions">
598
+ <button class="expand-all-btn">Collapse All</button>
599
+ <button class="sidebar-toggle">☰</button>
600
+ </div>
601
+ </div>
602
+ <div class="search-container">
603
+ <input type="search" class="search-box" placeholder="Search books or highlights..." aria-label="Search" />
604
+ </div>
605
+ <div class="stats">${totalHighlights} highlights from ${totalBooks} books • Exported ${exportDate}</div>
606
+ </header>
607
+
608
+ <div class="container">
609
+ ${renderBookIndex(books)}
610
+ <main>
611
+ `;
612
+ for (const book of books) {
613
+ html += renderBook(book);
614
+ }
615
+ html += ` </main>
616
+ </div>
617
+
618
+ <script>
619
+ // Initialize highlights heights for smooth transitions
620
+ document.addEventListener('DOMContentLoaded', () => {
621
+ document.querySelectorAll('.highlights').forEach(highlights => {
622
+ highlights.style.maxHeight = highlights.scrollHeight + 'px';
623
+ });
624
+ });
625
+
626
+ // Search functionality with debouncing
627
+ const searchInput = document.querySelector('.search-box');
628
+ let searchTimeout;
629
+
630
+ searchInput.addEventListener('input', (e) => {
631
+ clearTimeout(searchTimeout);
632
+ searchTimeout = setTimeout(() => {
633
+ const query = e.target.value.toLowerCase().trim();
634
+ const books = document.querySelectorAll('.book');
635
+
636
+ if (query === '') {
637
+ books.forEach(book => {
638
+ book.classList.remove('hidden');
639
+ book.style.opacity = '1';
640
+ });
641
+ } else {
642
+ books.forEach(book => {
643
+ const text = book.textContent.toLowerCase();
644
+ if (text.includes(query)) {
645
+ book.classList.remove('hidden');
646
+ book.style.opacity = '1';
647
+ } else {
648
+ book.style.opacity = '0';
649
+ setTimeout(() => book.classList.add('hidden'), 200);
650
+ }
651
+ });
652
+ }
653
+ }, 150);
654
+ });
655
+
656
+ // Expand/Collapse individual book
657
+ document.querySelectorAll('.book-header').forEach(header => {
658
+ header.addEventListener('click', (e) => {
659
+ const book = header.closest('.book');
660
+ book.classList.toggle('collapsed');
661
+
662
+ // Save state to localStorage
663
+ const bookId = book.id;
664
+ const isCollapsed = book.classList.contains('collapsed');
665
+ localStorage.setItem('book-' + bookId, isCollapsed ? 'collapsed' : 'expanded');
666
+ });
667
+ });
668
+
669
+ // Expand/Collapse All button
670
+ const expandAllBtn = document.querySelector('.expand-all-btn');
671
+ let allCollapsed = false;
672
+
673
+ expandAllBtn.addEventListener('click', () => {
674
+ const books = document.querySelectorAll('.book');
675
+ allCollapsed = !allCollapsed;
676
+
677
+ books.forEach(book => {
678
+ if (allCollapsed) {
679
+ book.classList.add('collapsed');
680
+ localStorage.setItem('book-' + book.id, 'collapsed');
681
+ } else {
682
+ book.classList.remove('collapsed');
683
+ localStorage.setItem('book-' + book.id, 'expanded');
684
+ }
685
+ });
686
+
687
+ expandAllBtn.textContent = allCollapsed ? 'Expand All' : 'Collapse All';
688
+ });
689
+
690
+ // Sidebar toggle
691
+ const sidebarToggle = document.querySelector('.sidebar-toggle');
692
+ const sidebar = document.querySelector('.sidebar');
693
+
694
+ sidebarToggle.addEventListener('click', () => {
695
+ sidebar.classList.toggle('visible');
696
+ const isVisible = sidebar.classList.contains('visible');
697
+ localStorage.setItem('sidebar-visible', isVisible ? 'true' : 'false');
698
+ });
699
+
700
+ // Close sidebar when clicking outside on mobile
701
+ document.addEventListener('click', (e) => {
702
+ if (window.innerWidth <= 1023) {
703
+ if (!sidebar.contains(e.target) && !sidebarToggle.contains(e.target)) {
704
+ sidebar.classList.remove('visible');
705
+ }
706
+ }
707
+ });
708
+
709
+ // Smooth scroll for anchor links with active state
710
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
711
+ anchor.addEventListener('click', function (e) {
712
+ e.preventDefault();
713
+ const target = document.querySelector(this.getAttribute('href'));
714
+ if (target) {
715
+ // Remove active class from all links
716
+ document.querySelectorAll('.book-link').forEach(link => {
717
+ link.classList.remove('active');
718
+ });
719
+
720
+ // Add active class to clicked link
721
+ this.classList.add('active');
722
+
723
+ // Expand book if collapsed
724
+ target.classList.remove('collapsed');
725
+ localStorage.setItem('book-' + target.id, 'expanded');
726
+
727
+ // Instant scroll
728
+ target.scrollIntoView({
729
+ behavior: 'instant',
730
+ block: 'start'
731
+ });
732
+
733
+ // Close sidebar on mobile
734
+ if (window.innerWidth <= 1023) {
735
+ sidebar.classList.remove('visible');
736
+ }
737
+ }
738
+ });
739
+ });
740
+
741
+ // Restore collapse states from localStorage
742
+ document.querySelectorAll('.book').forEach(book => {
743
+ const bookId = book.id;
744
+ const savedState = localStorage.getItem('book-' + bookId);
745
+ if (savedState === 'collapsed') {
746
+ book.classList.add('collapsed');
747
+ }
748
+ });
749
+
750
+ // Restore sidebar state from localStorage
751
+ const sidebarState = localStorage.getItem('sidebar-visible');
752
+ if (sidebarState === 'true' && window.innerWidth <= 1023) {
753
+ sidebar.classList.add('visible');
754
+ }
755
+
756
+ // Update expand/collapse all button text based on current state
757
+ function updateExpandAllButton() {
758
+ const books = document.querySelectorAll('.book');
759
+ const collapsedCount = document.querySelectorAll('.book.collapsed').length;
760
+ allCollapsed = collapsedCount === books.length;
761
+ expandAllBtn.textContent = allCollapsed ? 'Expand All' : 'Collapse All';
762
+ }
763
+
764
+ updateExpandAllButton();
765
+ </script>
766
+ </body>
767
+ </html>
768
+ `;
769
+ // Write to file
770
+ const fs = require('fs');
771
+ const path = require('path');
772
+ const resolvedPath = path.resolve(outputPath);
773
+ fs.writeFileSync(resolvedPath, html, 'utf-8');
774
+ return resolvedPath;
775
+ }