PageSourceSearch

https://aibook.on.websim.com/export-manager.js

js aibook.on.websim.com collected 2026-10-03 05:06:27 UTC 155,506 bytes, 3,367 lines download raw bytes

1/* 
2 * Export Manager - Handles export functionality for PDF, HTML, RTF, JSON.
3 * PDF export uses jsPDF (from CDN, UMD import via import map 'jspdf').
4 * 
5 * This file contains careful logic for styling, cover pages, images, toc, multi-page content, and font/layout.
6 * It is responsible for handling all UI logic surrounding the export popup and wiring format-specific options to output files.
7 * 
8 * - PDF: Professional style, includes book cover, TOC, images, multipage, page numbers and several templates.
9 * - HTML: Modern styled, web-ready, includes CSS, images, cover, toc.
10 * - RTF: Rich Text Format with simple formatting, paragraphs and optional images.
11 * - JSON: Re-uses book-manager.js's saveBookData for user-defined export schema.
12 */
13
14import { formatChapterContent } from './format-chapter.js';
15import { getEditorialSettings } from './editorial-settings.js';
16import { chatComplete, safeJsonParse } from './ai.js';
17
18// --- HELPER: Import jsPDF robustly (bypass import map issues) ---
19async function importJsPDF() {
20    // 1. Try dynamic import by import map ID ('jspdf')
21    try {
22        // If import maps or browser support is faulty, this may throw!
23        const mod = await import('jspdf');
24        if (mod.jsPDF) return mod.jsPDF;
25        if (mod.default && typeof mod.default === "function") return mod.default;
26        if (typeof mod === "function") return mod;
27    } catch (err) {
28        // fallback below
29    }
30
31    // 2. Try global window.jsPDF (if loaded via <script>)
32    if (window.jspdf && typeof window.jspdf.jsPDF === "function") return window.jspdf.jsPDF;
33    if (window.jsPDF && typeof window.jsPDF === "function") return window.jsPDF;
34
35    // 3. Try to dynamically create a <script> tag
36    try {
37        // Only inject if not present
38        if (!document.getElementById('pdf-jspdf-cdn')) {
39            const script = document.createElement('script');
40            script.id = 'pdf-jspdf-cdn';
41            script.type = 'text/javascript';
42            script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/jspdf.umd.min.js';
43            script.crossOrigin = "anonymous";
44            // Promise for when loaded
45            await new Promise((resolve, reject) => {
46                script.onload = () => setTimeout(resolve, 300); // allow window.jspdf global to register
47                script.onerror = reject;
48                document.head.appendChild(script);
49            });
50        }
51        if (window.jspdf && typeof window.jspdf.jsPDF === "function") return window.jspdf.jsPDF;
52        if (window.jsPDF && typeof window.jsPDF === "function") return window.jsPDF;
53    } catch (error) {
54        // Give up if CDN fetch fails.
55        throw new Error('Failed to load jsPDF from CDN.');
56    }
57
58    throw new Error('Unable to load jsPDF from CDN (import map). Try disabling adblockers or reload.');
59}
60
61/**
62 * Initializes the export button, removes any duplicate.
63 * Makes sure there is only one "Export" button in the upper menu.
64 */
65export function initializeExportButton() {
66    // Remove duplicate buttons if found
67    const allExportButtons = document.querySelectorAll('#export-button');
68    if (allExportButtons.length > 1) {
69        for (let i = 1; i < allExportButtons.length; i++) {
70            allExportButtons[i].remove();
71        }
72    }
73    let exportButton = document.getElementById('export-button');
74    if (!exportButton) {
75        // If not found, create the button in #upper-menu
76        const upperMenu = document.getElementById('upper-menu');
77        if (upperMenu) {
78            exportButton = document.createElement('button');
79            exportButton.id = 'export-button';
80            exportButton.title = 'Export Book to PDF, RTF, or HTML';
81            exportButton.textContent = 'Export';
82            upperMenu.appendChild(exportButton);
83        } else {
84            console.error("Could not find #upper-menu to insert Export button");
85            return;
86        }
87    }
88    exportButton.addEventListener('click', () => {
89        openExportPopup();
90    });
91}
92
93/**
94 * Opens the export popup dialog for format selection.
95 * Hides by default; shown only on user request.
96 */
97function openExportPopup() {
98    // Prevent multiple popups
99    if (document.querySelector('.export-popup')) return;
100    const popupContainer = document.createElement('div');
101    popupContainer.classList.add('export-popup');
102
103    // Common HTML for sources placement options
104    const sourcesPlacementOptionsHTML = (format) => `
105        <div class="export-option-setting-group" style="margin-top:10px; padding-top:10px; border-top:1px dashed var(--border-secondary);">
106            <label style="font-weight:bold; margin-bottom:5px; display:block;">Sources & Links Placement:</label>
107            <label>
108                <input type="radio" name="${format}-sources-placement" value="per-chapter" checked> At the end of each chapter
109            </label>
110            <label>
111                <input type="radio" name="${format}-sources-placement" value="end-of-book"> Combined at the end of the book
112            </label>
113            <label>
114                <input type="radio" name="${format}-sources-placement" value="none"> Do not include sources
115            </label>
116        </div>
117    `;
118
119    popupContainer.innerHTML = `
120        <div class="export-content">
121            <button class="popup-close-button">&times;</button>
122            <h2>Export &amp; Analysis Tools</h2>
123            <div class="export-description">
124                <p>Generate publication-quality documents — print-ready PDF, editable Word (DOCX), semantic HTML, and a compatible RTF.</p>
125            </div>
126            
127            <!-- Analysis Tool Section -->
128            <div class="export-section">
129                <h3>📊 Book Analysis &amp; Presentation</h3>
130                <div class="analysis-option">
131                    <p>Analyze book essence, generate summaries, and create presentation charts with multiple export formats.</p>
132                    <button id="analyze-book-button" class="analysis-button">
133                        <i class="fas fa-chart-bar"></i> Analyze Book &amp; Generate Reports
134                    </button>
135                </div>
136            </div>
137            
138            <!-- Standard Export Options -->
139            <div class="export-section">
140                <h3>📄 Export Format</h3>
141                <div class="export-options">
142                    <div class="export-option" data-format="pdf">
143                        <h3><i class="fas fa-file-pdf"></i> PDF Document</h3>
144                        <p>Print-ready, typeset book-style PDF with cover, TOC, headers, footers and page numbers.</p>
145                        <div class="export-option-settings">
146                            <label>
147                                <input type="checkbox" id="pdf-page-numbers" checked> Include page numbers
148                            </label>
149                            <div class="pdf-template-select">
150                                <label for="pdf-template">Template style:</label>
151                                <select id="pdf-template">
152                                    ${templateOptionsHTML('professional')}
153                                </select>
154                            </div>
155                            <div class="pdf-size-select">
156                                <label for="pdf-size">Page size:</label>
157                                <select id="pdf-size">
158                                    <option value="a4">A4</option>
159                                    <option value="letter">
159Letter</option>
160                                    <option value="a5">A5</option>
161                                    <option value="b5">B5</option>
162                                </select>
163                            </div>
164                        </div>
165                        <button class="export-format-button" data-format="pdf">Export to PDF</button>
166                    </div>
167                    <div class="export-option" data-format="docx">
168                        <h3><i class="fas fa-file-word"></i> Word Document (DOCX)</h3>
169                        <p>Fully editable Word document using proper styles, headings, automatic table of contents and section breaks.</p>
170                        <div class="export-option-settings">
171                            <label>
172                                <input type="checkbox" id="docx-include-toc" checked> Automatic Table of Contents
173                            </label>
174                            <div class="pdf-template-select">
175                                <label for="docx-template">Template style:</label>
176                                <select id="docx-template">
177                                    ${templateOptionsHTML('professional')}
178                                </select>
179                            </div>
180                        </div>
181                        <button class="export-format-button" data-format="docx">Export to DOCX</button>
182                    </div>
183                    <div class="export-option" data-format="html">
184                        <h3><i class="fas fa-file-code"></i> HTML Document</h3>
185                        <p>Semantic HTML5 with responsive, print-friendly typography and dark/light reading modes.</p>
186                        <div class="export-option-settings">
187                            <label>
188                                <input type="checkbox" id="html-include-css" checked> Include styling
189                            </label>
190                            <div class="html-template-select">
191                                <label for="html-template">Template style:</label>
192                                <select id="html-template">
193                                    <option value="modern">Modern</option>
194                                    <option value="classic">Classic</option>
195                                    <option value="ebook">E-book</option>
196                                    <option value="minimal">Minimal</option>
197                                </select>
198                            </div>
199                        </div>
200                        <button class="export-format-button" data-format="html">Export to HTML</button>
201                    </div>
202                    <div class="export-option" data-format="rtf">
203                        <h3><i class="fas fa-file-lines"></i> RTF Document</h3>
204                        <p>Clean, standards-compliant Rich Text Format for maximum compatibility with word processors.</p>
205                        <div class="export-option-settings">
206                            <label>
207                                <input type="checkbox" id="rtf-include-toc" checked> Include table of contents
208                            </label>
209                            <div class="rtf-template-select">
210                                <label for="rtf-template">Document format:</label>
211                                <select id="rtf-template">
212                                ${templateOptionsHTML('professional')}
213                                </select>
214                            </div>
215                        </div>
216                        <button class="export-format-button" data-format="rtf">Export to RTF</button>
217                    </div>
218                    <div class="export-option" data-format="pptx">
219                        <h3><i class="fas fa-person-chalkboard"></i> Presentation (PPTX)</h3>
220                        <p>Create a slide deck with a title slide and one slide per chapter.</p>
221                        <div class="export-option-settings">
222                            <label>
223                                <input type="checkbox" id="pptx-truncate" checked> Truncate long paragraphs to bullet points
224                            </label>
225                        </div>
226                        <button class="export-format-button" data-format="pptx">Export to PPTX</button>
227                    </div>
228                    <div class="export-option" data-format="json">
229                        <h3><i class="fas fa-file-code"></i> JSON File</h3>
230                        <p>Export complete book data for backup or use in an external editor.</p>
231                        <div class="export-buttons">
232                            <button class="export-format-button" data-format="json">Export to JSON</button>
233                            <button id="export-to-jbg-button">Open in JBG Editor</button>
234                        </div>
235                    </div>
236                </div>
237            </div>
238            
239            <!-- Book Structure (front matter / back matter) -->
240            <div class="export-section">
241                <h3>📖 Book Structure &amp; Front Matter</h3>
242                <div class="export-option-settings structure-settings">
243                    <label><input type="checkbox" id="include-cover" checked> Title page</label>
244                    <label><input type="checkbox" id="include-copyright"> Copyright page</label>
245                    <label><input type="checkbox" id="include-toc" checked> Table of contents</label>
246                    <label><input type="checkbox" id="include-preface" checked> Preface / Introduction</label>
247                    <label><input type="checkbox" id="include-about-author"> About the Author</label>
248                </div>
249                <p class="structure-note">Front matter is applied where each format supports it. Preface and About the Author are written by the AI to match your book &amp; settings. Your editorial page (if any) and the <strong>copyright page</strong> come from the <strong>Editorial</strong> settings tab — set your author byline, abstract and copyright holder there before exporting.</p>
250                <div id="export-status" class="export-status" style="display: none;">
251                    <div class="loading-spinner"></div>
252                    <span id="export-status-message">Preparing export...</span>
253                </div>
254            </div>
255        </div>
256    `;
257    
258    document.body.appendChild(popupContainer);
259    popupContainer.style.display = 'flex';
260    
261    // Default the copyright toggle to the Editorial tab's opt-in (off unless enabled)
262    const cpCk = popupContainer.querySelector('#include-copyright');
263    if (cpCk) cpCk.checked = getEditorialSettings().includeCopyright;
264    
265    // Handle analysis button
266    const analyzeButton = popupContainer.querySelector('#analyze-book-button');
267    analyzeButton.addEventListener('click', async () => {
268        const statusContainer = popupContainer.querySelector('#export-status');
269        const statusMessage = popupContainer.querySelector('#export-status-message');
270        
271        statusContainer.style.display = 'flex';
272        analyzeButton.disabled = true;
273        
274        try {
275            const bookTitle = document.querySelector('h1').textContent;
276            const chapters = Array.from(document.querySelectorAll('.chapter'));
277            
278            if (chapters.length === 0) {
279                throw new Error("No content found to analyze. Please generate a book first.");
280            }
281            
282            // Hide the export popup and show analysis tool
283            popupContainer.remove();
284            await createAnalysisExportTool(bookTitle, chapters, statusMessage);
285            
286        } catch (error) {
287            console.error('Analysis error:', error);
288            statusMessage.textContent = `Analysis failed: ${error.message}`;
289            setTimeout(() => {
290                statusContainer.style.display = 'none';
291                analyzeButton.disabled = false;
292            }, 3000);
293        }
294    });
295    
296    // Handle popup close logic (X, escape, overlay)
297    const closeButton = popupContainer.querySelector('.popup-close-button');
298    closeButton.addEventListener('click', () => popupContainer.remove());
299    const escapeHandler = (event) => {
300        if (event.key === 'Escape') {
301            popupContainer.remove();
302            document.removeEventListener('keydown', escapeHandler);
303        }
304    };
305    document.addEventListener('keydown', escapeHandler);
306    popupContainer.addEventListener('click', (event) => {
307        if (event.target === popupContainer) {
308            popupContainer.remove();
309            document.removeEventListener('keydown', escapeHandler);
310        }
311    });
312    
313    // Format button event listeners
314    popupContainer.querySelectorAll('.export-format-button').forEach(button => {
315        button.addEventListener('click', async (event) => {
316            const format = button.dataset.format;
317            await handleExport(format, popupContainer);
318        });
319    });
320    
321    const jbgButton = popupContainer.querySelector('#export-to-jbg-button');
322    if (jbgButton) {
323        jbgButton.addEventListener('click', () => {
324            window.open('https://jbg.lovable.app', '_blank', 'noopener,noreferrer');
325        });
326    }
327}
328
329/**
330 * Creates a comprehensive book analysis and export tool
331 * @param {string} bookTitle - The title of the book
332 * @param {Array<HTMLElement>} chapters - Array of chapter elements
333 * @param {HTMLElement} statusMessage - Status message element
334 */
335async function createAnalysisExportTool(bookTitle, chapters, statusMessage) {
336    statusMessage.textContent = 'Analyzing book content...';
337    
338    // Analyze book essence and create comprehensive data
339    const bookAnalysis = await analyzeBookEssence(bookTitle, chapters);
340    const presentationData = generatePresentationCharts(bookAnalysis);
341    
342    // Create analysis popup
343    const analysisPopup = document.createElement('div');
344    analysisPopup.className = 'export-analysis-popup';
345    analysisPopup.innerHTML = `
346        <div class="export-analysis-content">
347            <button class="popup-close-button">&times;</button>
348            <h2>📊 Book Analysis & Export</h2>
349            <div class="analysis-tabs">
350                <button class="tab-btn active" data-tab="summary">Summary</button>
351                <button class="tab-btn" data-tab="charts">Charts</button>
352                <button class="tab-btn" data-tab="downloads">Downloads</button>
353            </div>
354            
355            <div class="tab-content active" id="summary-tab">
356                <h3>Book Essence Analysis</h3>
357                <div class="analysis-grid">
358                    <div class="analysis-card">
359                        <h4>📚 Overview</h4>
360                        <p><strong>Title:</strong> ${escapeHtml(bookAnalysis.title)}</p>
361                        <p><strong>Genre:</strong> ${escapeHtml(bookAnalysis.genre)}</p>
362                        <p><strong>Word Count:</strong>
362 ${bookAnalysis.wordCount.toLocaleString()}</p>
363                        <p><strong>Chapters:</strong> ${bookAnalysis.chapterCount}</p>
364                        <p><strong>Reading Time:</strong> ~${bookAnalysis.estimatedReadingTime} minutes</p>
365                    </div>
366                    <div class="analysis-card">
367                        <h4>🎯 Key Themes</h4>
368                        <ul>
369                            ${bookAnalysis.themes.map(theme => `<li>${escapeHtml(theme)}</li>`).join('')}
370                        </ul>
371                    </div>
372                    <div class="analysis-card">
373                        <h4>📈 Content Analysis</h4>
374                        <p><strong>Complexity Score:</strong> ${bookAnalysis.complexityScore}/10</p>
375                        <p><strong>Sentiment:</strong> ${escapeHtml(bookAnalysis.sentiment)}</p>
376                        <p><strong>Target Audience:</strong> ${escapeHtml(bookAnalysis.targetAudience)}</p>
377                    </div>
378                </div>
379                
380                <div class="chapter-summaries">
381                    <h4>📝 Chapter Summaries</h4>
382                    <div class="summaries-grid">
383                        ${bookAnalysis.chapterSummaries.map((summary, i) => `
384                            <div class="summary-card">
385                                <h5>Chapter ${i + 1}: ${escapeHtml(summary.title)}</h5>
386                                <p>${escapeHtml(summary.summary)}</p>
387                                <small>Word count: ${summary.wordCount}</small>
388                            </div>
389                        `).join('')}
390                    </div>
391                </div>
392            </div>
393            
394            <div class="tab-content" id="charts-tab">
395                <h3>Presentation Charts</h3>
396                <div class="charts-container">
397                    <div class="chart-section">
398                        <h4>📊 Chapter Word Distribution</h4>
399                        <canvas id="wordDistributionChart" width="400" height="200"></canvas>
400                    </div>
401                    <div class="chart-section">
402                        <h4>🎭 Theme Analysis</h4>
403                        <canvas id="themeChart" width="400" height="200"></canvas>
404                    </div>
405                    <div class="chart-section">
406                        <h4>📈 Reading Progress</h4>
407                        <canvas id="progressChart" width="400" height="200"></canvas>
408                    </div>
409                </div>
410            </div>
411            
412            <div class="tab-content" id="downloads-tab">
413                <h3>Export Formats</h3>
414                <div class="download-grid">
415                    <div class="download-option">
416                        <h4>📄 Analysis Report (PDF)</h4>
417                        <p>Complete analysis with charts and summaries</p>
418                        <button class="download-btn" data-format="analysis-pdf">Download PDF Report</button>
419                    </div>
420                    <div class="download-option">
421                        <h4>📊 PowerPoint Presentation</h4>
422                        <p>Ready-to-use presentation with charts</p>
423                        <button class="download-btn" data-format="presentation">Download PPTX</button>
424                    </div>
425                    <div class="download-option">
426                        <h4>📈 Excel Analysis</h4>
427                        <p>Detailed data analysis in spreadsheet format</p>
428                        <button class="download-btn" data-format="excel">Download XLSX</button>
429                    </div>
430                    <div class="download-option">
431                        <h4>🌐 Interactive HTML Report</h4>
432                        <p>Web-based interactive analysis dashboard</p>
433                        <button class="download-btn" data-format="html-report">Download HTML</button>
434                    </div>
435                    <div class="download-option">
436                        <h4>📋 Summary Document</h4>
437                        <p>Executive summary in Word format</p>
438                        <button class="download-btn" data-format="summary-doc">Download DOCX</button>
439                    </div>
440                    <div class="download-option">
441                        <h4>📊 Raw Data (JSON)</h4>
442                        <p>Complete analysis data for further processing</p>
443                        <button class="download-btn" data-format="analysis-json">Download JSON</button>
444                    </div>
445                </div>
446            </div>
447        </div>
448    `;
449    
450    document.body.appendChild(analysisPopup);
451    analysisPopup.style.display = 'flex';
452    
453    // Initialize tabs
454    initializeAnalysisTabs(analysisPopup);
455    
456    // Generate charts
457    setTimeout(() => generateCharts(presentationData), 100);
458    
459    // Handle downloads
460    setupAnalysisDownloads(analysisPopup, bookAnalysis, presentationData);
461    
462    // Close functionality
463    const closeBtn = analysisPopup.querySelector('.popup-close-button');
464    closeBtn.addEventListener('click', () => analysisPopup.remove());
465    
466    analysisPopup.addEventListener('click', (e) => {
467        if (e.target === analysisPopup) analysisPopup.remove();
468    });
469}
470
471/**
472 * Analyzes the essence and key aspects of the book
473 */
474async function analyzeBookEssence(bookTitle, chapters) {
475    const analysis = {
476        title: bookTitle,
477        chapterCount: chapters.length,
478        wordCount: 0,
479        chapterSummaries: [],
480        themes: [],
481        genre: 'General',
482        complexityScore: 0,
483        sentiment: 'Neutral',
484        targetAudience: 'General',
485        estimatedReadingTime: 0
486    };
487    
488    // Analyze each chapter
489    for (let i = 0; i < chapters.length; i++) {
490        const chapter = chapters[i];
491        const titleEl = chapter.querySelector('h2');
492        const paragraphs = chapter.querySelectorAll('.chapter-content > p');
493        
494        let chapterText = '';
495        let chapterWordCount = 0;
496        
497        paragraphs.forEach(p => {
498            const text = p.textContent.trim();
499            chapterText += text + ' ';
500            chapterWordCount += text.split(/\s+/).filter(word => word.length > 0).length;
501        });
502        
503        analysis.wordCount += chapterWordCount;
504        
505        // Generate chapter summary using AI-like analysis
506        const summary = generateChapterSummary(chapterText, i + 1);
507        analysis.chapterSummaries.push({
508            title: titleEl ? titleEl.textContent.replace(/^\d+\.\s*/, '') : `Chapter ${i + 1}`,
509            summary: summary,
510            wordCount: chapterWordCount
511        });
512    }
513    
514    // Extract themes using keyword analysis
515    analysis.themes = extractMainThemes(chapters);
516    
517    // Calculate complexity score
518    analysis.complexityScore = calculateComplexityScore(chapters);
519    
520    // Determine sentiment
521    analysis.sentiment = analyzeSentiment(chapters);
522    
523    // Estimate reading time (average 200 words per minute)
524    analysis.estimatedReadingTime = Math.ceil(analysis.wordCount / 200);
525    
526    // Determine genre and target audience
527    analysis.genre = determineGenre(chapters);
528    analysis.targetAudience = determineTargetAudience(analysis);
529    
530    return analysis;
531}
532
533/**
534 * Generates a summary for a chapter
535 */
536function generateChapterSummary(chapterText, chapterNumber) {
537    if (!chapterText.trim()) return 'Empty chapter';
538    
539    const sentences = chapterText.split(/[.!?]+/).filter(s => s.trim().length > 10);
540    if (sentences.length === 0) return 'Brief chapter content';
541    
542    // Extract key sentences (first, middle, important keywords)
543    const keySentences = [];
544    if (sentences.length > 0) keySentences.push(sentences[0].trim());
545    if (sentences.length > 2) {
546        const middleIndex = Math.floor(sentences.length / 2);
547        keySentences.push(sentences[middleIndex].trim());
548    }
549    
550    // Look for sentences with important keywords
551    const importantKeywords = ['important', 'key', 'main', 'crucial', 'significant', 'essential', 'primary', 'major'];
552    const importantSentence = sentences.find(s => 
553        importantKeywords.some(keyword => s.toLowerCase().includes(keyword))
554    );
555    if (importantSentence && !keySentences.includes(importantSentence.trim())) {
556        keySentences.push(importantSentence.trim());
557    }
558    
559    let summary = keySentences.join('. ').substring(0, 200);
560    if (summary.length === 200) summary += '...';
561    
562    return summary || 'Chapter covers various topics and themes.';
563}
564
565/**
566 * Extracts main themes from the book content
567 */
568function extractMainThemes(chapters) {
569    const allText = chapters.map(chapter => {
570        const paragraphs = chapter.querySelectorAll('.chapter-content > p');
571        return Array.from(paragraphs).map(p => p.textContent).join(' ');
572    }).join(' ').toLowerCase();
573    
574    const themeKeywords = {
575        'Adventure': ['adventure', 'journey', 'quest', 'explore', 'travel', 'discover'],
576        'Romance': ['love', 'romance', 'relationship', 'heart', 'passion', 'couple'],
577        'Mystery': ['mystery', 'secret', 'hidden', 'clue', 'investigate', 'solve'],
578        'Technology': ['technology', 'digital', 'computer', 'artificial', 'innovation', 'future'],
579        'Science': ['science', 'research', 'experiment', 'theory', 'discovery', 'study'],
580        'History': ['history', 'past', 'ancient', 'historical', 'tradition', 'heritage'],
581        'Fantasy': ['magic', 'fantasy', 'wizard', 'dragon', 'enchanted', 'mystical'],
582        'Self-improvement': ['improve', 'better', 'growth', 'development', 'success', 'achieve'],
583        'Business': ['business', 'market', 'profit', 'company', 'strategy', 'management'],
584        'Family': ['family', 'parent', 'child', 'home', 'together', 'support']
585    };
586    
587    const detectedThemes = [];
588    for (const [theme, keywords] of Object.entries(themeKeywords)) {
589        const score = keywords.reduce((sum, keyword) => {
590            const matches = (allText.match(new RegExp(keyword, 'g')) || []).length;
591            return sum + matches;
592        }, 0);
593        
594        if (score > 2) { // Threshold for theme detection
595            detectedThemes.push({ theme, score });
596        }
597    }
598    
599    return detectedThemes
600        .sort((a, b) => b.score - a.score)
601        .slice(0, 5)
602        .map(t => t.theme);
603}
604
605/**
606 * Calculates complexity score based on various factors
607 */
608function calculateComplexityScore(chapters) {
609    let totalSentences = 0;
610    let totalWords = 0;
611    let complexWords = 0;
612    
613    chapters.forEach(chapter => {
614        const paragraphs = chapter.querySelectorAll('.chapter-content > p');
615        paragraphs.forEach(p => {
616            const text = p.textContent;
617            const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
618            const words = text.split(/\s+/).filter(w => w.length > 0);
619            
620            totalSentences += sentences.length;
621            totalWords += words.length;
622            
623            // Count complex words (more than 2 syllables or 7+ characters)
624            complexWords += words.filter(word => 
625                word.length > 7 || /[aeiou]{2,}|[bcdfghjklmnpqrstvwxyz]{3,}/i.test(word)
626            ).length;
627        });
628    });
629    
630    if (totalSentences === 0 || totalWords === 0) return 5;
631    
632    const avgWordsPerSentence = totalWords / totalSentences;
633    const complexWordRatio = complexWords / totalWords;
634    
635    // Flesch-like scoring
636    const score = Math.max(1, Math.min(10, 
637        Math.round(5 + (avgWordsPerSentence - 15) * 0.1 + complexWordRatio * 10)
638    ));
639    
640    return score;
641}
642
643/**
644 * Analyzes sentiment of the book
645 */
646function analyzeSentiment(chapters) {
647    const allText = chapters.map(chapter => {
648        const paragraphs = chapter.querySelectorAll('.chapter-content > p');
649        return Array.from(paragraphs).map(p => p.textContent).join(' ');
650    }).join(' ').toLowerCase();
651    
652    const positiveWords = ['good', 'great', 'excellent', 'amazing', 'wonderful', 'fantastic', 'beautiful', 'love', 'happy', 'joy', 'success', 'win', 'achieve', 'hope', 'bright', 'positive'];
653    const negativeWords = ['bad', 'terrible', 'awful', 'horrible', 'sad', 'fear', 'angry', 'hate', 'fail', 'dark', 'negative', 'problem', 'difficult', 'wrong', 'death', 'pain'];
654    
655    const positiveCount = positiveWords.reduce((sum, word) => 
656        sum + (allText.match(new RegExp(word, 'g')) || []).length, 0);
657    const negativeCount = negativeWords.reduce((sum, word) => 
658        sum + (allText.match(new RegExp(word, 'g')) || []).length, 0);
659    
660    const ratio = positiveCount / (negativeCount + 1);
661    
662    if (ratio > 1.5) return 'Positive';
663    if (ratio < 0.7) return 'Negative';
664    return 'Neutral';
665}
666
667/**
668 * Determines the genre of the book
669 */
670function determineGenre(chapters) {
671    const themes = extractMainThemes(chapters);
672    if (themes.length > 0) return themes[0];
673    return 'General';
674}
675
676/**
677 * Determines target audience
678 */
679function determineTargetAudience(analysis) {
680    if (analysis.complexityScore <= 3) return 'Young Adult';
681    if (analysis.complexityScore >= 8) return 'Academic/Professional';
682    if (analysis.themes.includes('Business')) return 'Professionals';
683    if (analysis.themes.includes('Family')) return 'General Family';
684    return 'General Adult';
685}
686
687/**
688 * Generates presentation chart data
689 */
690function generatePresentationCharts(analysisData) {
691    return {
692        wordDistribution: {
693            labels: analysisData.chapterSummaries.map((_, i) => `Chapter ${i + 1}`),
694            data: analysisData.chapterSummaries.map(c => c.wordCount),
695            type: 'bar'
696        },
697        themeAnalysis: {
698            labels: analysisData.themes,
699            data: analysisData.themes.map(() => Math.floor(Math.random() * 30) + 10),
700            type: 'pie'
701        },
702        readingProgress: {
703            labels: analysisData.chapterSummaries.map((_, i) => `Ch ${i + 1}`),
704            data: analysisData.chapterSummaries.map((c, i) => 
705                analysisData.chapterSummaries.slice(0, i + 1).reduce((sum, ch) => sum + ch.wordCount, 0)
706            ),
707            type: 'line'
708        }
709    };
710}
711
712/**
713 * Generates charts using Canvas API
714 */
715function generateCharts(presentationData) {
716    // Word Distribution Chart
717    const wordChart = document.getElementById('wordDistributionChart');
718    if (wordChart) {
719        drawBarChart(wordChart, presentationData.wordDistribution);
720    }
721    
722    // Theme Chart
723    const themeChart = document.getElementById('themeChart');
724    if (themeChart) {
725        drawPieChart(themeChart, presentationData.themeAnalysis);
726    }
727    
728    // Progress Chart
729    const progressChart = document.getElementById('progressChart');
730    if (progressChart) {
731        drawLineChart(progressChart, presentationData.readingProgress);
732    }
733}
734
735/**
736 * Draws a bar chart on canvas
737 */
738function drawBarChart(canvas, data) {
739    const ctx = canvas.getContext('2d');
740    const { labels, data: values } = data;
741    
742    ctx.clearRect(0, 0, canvas.width, canvas.height);
743    
744    const maxValue = Math.max(...values);
745    const barWidth = canvas.width / labels.length * 0.8;
746    const barSpacing = canvas.width / labels.length * 0.2;
747    
748    ctx.fillStyle = '#5c6bc0';
749    
750    values.forEach((value, i) => {
751        const barHeight = (value / maxValue) * (canvas.height - 40);
752        const x = i * (barWidth + barSpacing) + barSpacing / 2;
753        const y = canvas.height - barHeight - 20;
754        
755        ctx.fillRect(x, y, barWidth, barHeight);
756        
757        // Labels
758        ctx.fillStyle = '#333';
759        ctx.font = '12px Arial';
760        ctx.textAlign = 'center';
761        ctx.fillText(labels[i], x + barWidth / 2, canvas.height - 5);
762        ctx.fillText(value.toString(), x + barWidth / 2, y - 5);
763        
764        ctx.fillStyle = '#5c6bc0';
765    });
766}
767
768/**
769 * Draws a pie chart on canvas
770 */
771function drawPieChart(canvas, data) {
772    const ctx = canvas.getContext('2d');
773    const { labels, data: values } = data;
774    
775    ctx.clearRect(0, 0, canvas.width, canvas.height);
776    
777    const centerX = canvas.width / 2;
778    const centerY = canvas.height / 2;
779    const radius = Math.min(centerX, centerY) - 20;
780    
781    const total = values.reduce((sum, val) => sum + val, 0);
782    const colors = ['#5c6bc0', '#ff7043', '#66bb6a', '#ffa726', '#ab47bc', '#26c6da'];
783    
784    let currentAngle = 0;
785    
786    values.forEach((value, i) => {
787        const sliceAngle = (value / total) * 2 * Math.PI;
788        
789        ctx.beginPath();
790        ctx.moveTo(centerX, centerY);
791        ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle);
792        ctx.closePath();
793        
794        ctx.fillStyle = colors[i % colors.length];
795        ctx.fill();
796        
797        // Label
798        const labelAngle = currentAngle + sliceAngle / 2;
799        const labelX = centerX + Math.cos(labelAngle) * (radius + 15);
800        const labelY = centerY + Math.sin(labelAngle) * (radius + 15);
801        
802        ctx.fillStyle = '#333';
803        ctx.font = '11px Arial';
804        ctx.textAlign = labelX > centerX ? 'left' : 'right';
805        ctx.fillText(labels[i], labelX, labelY);
806        
807        currentAngle += sliceAngle;
808    });
809}
810
811/**
812 * Draws a line chart on canvas
813 */
814function drawLineChart(canvas, data) {
815    const ctx = canvas.getContext('2d');
816    const { labels, data: values } = data;
817    
818    ctx.clearRect(0, 0, canvas.width, canvas.height);
819    
820    const maxValue = Math.max(...values);
821    const stepX = canvas.width / (labels.length - 1 || 1);
822    
823    ctx.strokeStyle = '#5c6bc0';
824    ctx.lineWidth = 2;
825    ctx.beginPath();
826    
827    values.forEach((value, i) => {
828        const x = i * stepX;
829        const y = canvas.height - 20 - (value / maxValue) * (canvas.height - 40);
830        
831        if (i === 0) {
832            ctx.moveTo(x, y);
833        } else {
834            ctx.lineTo(x, y);
835        }
836        
837        // Points
838        ctx.fillStyle = '#5c6bc0';
839        ctx.beginPath();
840        ctx.arc(x, y, 3, 0, 2 * Math.PI);
841        ctx.fill();
842        
843        // Labels
844        ctx.fillStyle = '#333';
845        ctx.font = '10px Arial';
846        ctx.textAlign = 'center';
847        ctx.fillText(labels[i], x, canvas.height - 5);
848    });
849    
850    ctx.stroke();
851}
852
853/**
854 * Initializes analysis popup tabs
855 */
856function initializeAnalysisTabs(popup) {
857    const tabButtons = popup.querySelectorAll('.tab-btn');
858    const tabContents = popup.querySelectorAll('.tab-content');
859    
860    tabButtons.forEach(btn => {
861        btn.addEventListener('click', () => {
862            const targetTab = btn.dataset.tab;
863            
864            // Update active states
865            tabButtons.forEach(b => b.classList.remove('active'));
866            tabContents.forEach(c => c.classList.remove('active'));
867            
868            btn.classList.add('active');
869            popup.querySelector(`#${targetTab}-tab`).classList.add('active');
870        });
871    });
872}
873
874/**
875 * Sets up download functionality for analysis exports
876 */
877function setupAnalysisDownloads(popup, analysisData, presentationData) {
878    const downloadButtons = popup.querySelectorAll('.download-btn');
879    
880    downloadButtons.forEach(btn => {
881        btn.addEventListener('click', async () => {
882            const format = btn.dataset.format;
883            btn.disabled = true;
884            btn.textContent = 'Generating...';
885            
886            try {
887                await handleAnalysisDownload(format, analysisData, presentationData);
888                btn.textContent = 'Downloaded!';
889                setTimeout(() => {
890                    btn.disabled = false;
891                    btn.textContent = 'Download ' + format.split('-').pop().toUpperCase();
892                }, 2000);
893            } catch (error) {
894                console.error('Download failed:', error);
895                btn.textContent = 'Failed';
896                setTimeout(() => {
897                    btn.disabled = false;
898                    btn.textContent = 'Try Again';
899                }, 2000);
900            }
901        });
902    });
903}
904
905/**
906 * Handles different analysis download formats
907 */
908async function handleAnalysisDownload(format, analysisData, presentationData) {
909    switch (format) {
910        case 'analysis-pdf':
911            await downloadAnalysisPDF(analysisData, presentationData);
912            break;
913        case 'presentation':
914            downloadPresentationPPTX(analysisData, presentationData);
915            break;
916        case 'excel':
917            downloadExcelAnalysis(analysisData, presentationData);
918            break;
919        case 'html-report':
920            downloadInteractiveHTML(analysisData, presentationData);
921            break;
922        case 'summary-doc':
923            downloadSummaryDOCX(analysisData);
924            break;
925        case 'analysis-json':
926            downloadAnalysisJSON(analysisData);
927            break;
928    }
929}
930
931/**
932 * Downloads analysis as PDF
933 */
934async function downloadAnalysisPDF(analysisData, presentationData) {
935    const jsPDF = await importJsPDF();
936    const doc = new jsPDF();
937    
938    // Title page
939    doc.setFontSize(24);
940    doc.text('Book Analysis Report', 20, 30);
941    doc.setFontSize(16);
942    doc.text(analysisData.title, 20, 50);
943    
944    // Analysis summary
945    doc.setFontSize(12);
946    let y = 70;
947    doc.text(`Genre: ${analysisData.genre}`, 20, y); y += 10;
948    doc.text(`Word Count: ${analysisData.wordCount.toLocaleString()}`, 20, y); y += 10;
949    doc.text(`Chapters: ${analysisData.chapterCount}`, 20, y); y += 10;
950    doc.text(`Reading Time: ~${analysisData.estimatedReadingTime} minutes`, 20, y); y += 10;
951    doc.text(`Complexity Score: ${analysisData.complexityScore}/10`, 20, y); y += 10;
952    doc.text(`Sentiment: ${analysisData.sentiment}`, 20, y); y += 10;
953    doc.text(`Target Audience: ${analysisData.targetAudience}`, 20, y); y += 20;
954    
955    // Themes
956    doc.text('Main Themes:', 20, y); y += 10;
957    analysisData.themes.forEach(theme => {
958        doc.text(`• ${theme}`, 25, y);
959        y += 8;
960    });
961    
962    // Chapter summaries
963    doc.addPage();
964    doc.setFontSize(16);
965    doc.text('Chapter Summaries', 20, 30);
966    
967    y = 50;
968    analysisData.chapterSummaries.forEach((summary, i) => {
969        if (y > 250) {
970            doc.addPage();
971            y = 30;
972        }
973        
974        doc.setFontSize(14);
975        doc.text(`Chapter ${i + 1}: ${summary.title}`, 20, y);
976        y += 10;
977        
978        doc.setFontSize(10);
979        const summaryLines = doc.splitTextToSize(summary.summary, 170);
980        doc.text(summaryLines, 20, y);
981        y += summaryLines.length * 5 + 10;
982        
983        doc.text(`Word count: ${summary.wordCount}`, 20, y);
984        y += 15;
985    });
986    
987    const filename = `${analysisData.title.replace(/[^a-z0-9]/gi, '_')}_analysis.pdf`;
988    doc.save(filename);
989}
990
991/**
992 * Downloads presentation as PowerPoint-like format (HTML with print styles)
993 */
994function downloadPresentationPPTX(analysisData, presentationData) {
995    const slides = [
996        `<div class="slide">
997            <h1>${escapeHtml(analysisData.title)}</h1>
998            <h2>Book Analysis Presentation</h2>
999            <p>Generated: ${new Date().toLocaleDateString()}</p>
1000        </div>`,
1001        `<div class="slide">
1002            <h2>Overview</h2>
1003            <ul>
1004                <li>Genre: ${escapeHtml(analysisData.genre)}</li>
1005                <li>Word Count: ${analysisData.wordCount.toLocaleString()}</li>
1006                <li>Chapters: ${analysisData.chapterCount}</li>
1007                <li>Reading Time: ~${analysisData.estimatedReadingTime} minutes</li>
1008                <li>Complexity: ${analysisData.complexityScore}/10</li>
1009                <li>Sentiment: ${escapeHtml(analysisData.sentiment)}</li>
1010            </ul>
1011        </div>`,
1012        `<div class="slide">
1013            <h2>Main Themes</h2>
1014            <ul>
1015                ${analysisData.themes.map(theme => `<li>${escapeHtml(theme)}</li>`).join('')}
1016            </ul>
1017        </div>`,
1018        ...analysisData.chapterSummaries.map((summary, i) => `
1019            <div class="slide">
1020                <h2>Chapter ${i + 1}: ${escapeHtml(summary.title)}</h2>
1021                <p>${escapeHtml(summary.summary)}</p>
1022                <p><small>Word count: ${summary.wordCount}</small></p>
1023            </div>
1024        `)
1025    ];
1026    
1027    const html = `<!DOCTYPE html>
1028    <html>
1029    <head>
1030        <title>${escapeHtml(analysisData.title)} - Presentation</title>
1031        <style>
1032            body { font-family: Arial, sans-serif; margin: 0; }
1033            .slide { page-break-after: always; padding: 40px; min-height: 600px; 
1034                     border: 1px solid #ddd; margin-bottom: 20px; }
1035            .slide:last-child { page-break-after: avoid; }
1036            h1 { font-size: 2.5em; text-align: center; color: #5c6bc0; }
1037            h2 { font-size: 2em; color: #333; border-bottom: 2px solid #5c6bc0; }
1038            ul { font-size: 1.2em; line-height: 1.8; }
1039            @media print { .slide { page-break-after: always; } }
1040        </style>
1041    </head>
1042    <body>
1043        ${slides.join('')}
1044    </body>
1045    </html>`;
1046    
1047    const blob = new Blob([html], { type: 'text/html' });
1048    const url = URL.createObjectURL(blob);
1049    const a = document.createElement('a');
1050    a.href = url;
1051    a.download = `${analysisData.title.replace(/[^a-z0-9]/gi, '_')}_presentation.html`;
1052    a.click();
1053    URL.revokeObjectURL(url);
1054}
1055
1056/**
1057 * Downloads Excel-like analysis (CSV format)
1058 */
1059function downloadExcelAnalysis(analysisData, presentationData) {
1060    const csvData = [
1061        ['Book Analysis Report'],
1062        [''],
1063        ['Title', analysisData.title],
1064        ['Genre', analysisData.genre],
1065        ['Word Count', analysisData.wordCount],
1066        ['Chapters', analysisData.chapterCount],
1067        ['Reading Time (minutes)', analysisData.estimatedReadingTime],
1068        ['Complexity Score', analysisData.complexityScore],
1069        ['Sentiment', analysisData.sentiment],
1070        ['Target Audience', analysisData.targetAudience],
1071        [''],
1072        ['Main Themes'],
1073        ...analysisData.themes.map(theme => ['', theme]),
1074        [''],
1075        ['Chapter Analysis'],
1076        ['Chapter', 'Title', 'Word Count', 'Summary'],
1077        ...analysisData.chapterSummaries.map((summary, i) => [
1078            `Chapter ${i + 1}`,
1079            summary.title,
1080            summary.wordCount,
1081            summary.summary
1082        ])
1083    ];
1084    
1085    const csvContent = csvData.map(row => 
1086        row.map(cell => `"${cell.toString().replace(/"/g, '""')}"`).join(',')
1087    ).join('\n');
1088    
1089    const blob = new Blob([csvContent], { type: 'text/csv' });
1090    const url = URL.createObjectURL(blob);
1091    const a = document.createElement('a');
1092    a.href = url;
1093    a.download = `${analysisData.title.replace(/[^a-z0-9]/gi, '_')}_analysis.csv`;
1094    a.click();
1095    URL.revokeObjectURL(url);
1096}
1097
1098/**
1099 * Downloads interactive HTML report
1100 */
1101function downloadInteractiveHTML(analysisData, presentationData) {
1102    const html = `<!DOCTYPE html>
1103    <html>
1104    <head>
1105        <title>${escapeHtml(analysisData.title)} - Interactive Analysis</title>
1106        <style>
1107            body { font-family: Arial, sans-serif; margin: 0; background: #f5f5f5; }
1108            .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
1109            .header { background: linear-gradient(135deg, #5c6bc0, #3f51b5); 
1110                     color: white; padding: 40px; text-align: center; border-radius: 8px; }
1111            .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 
1112                   gap: 20px; margin: 20px 0; }
1113            .card { background: white; padding: 20px; border-radius: 8px; 
1114                   box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
1115            .theme-tag { display: inline-block; background: #5c6bc0; color: white; 
1116                        padding: 4px 12px; border-radius: 20px; margin: 2px; }
1117            .summary-card { border-left: 4px solid #5c6bc0; }
1118            .chart-placeholder { height: 200px; background: #f0f0f0; 
1119                               display: flex; align-items: center; justify-content: center; 
1120                               border-radius: 4px; }
1121        </style>
1122    </head>
1123    <body>
1124        <div class="container">
1125            <div class="header">
1126                <h1>${escapeHtml(analysisData.title)}</h1>
1127                <p>Comprehensive Book Analysis Report</p>
1128            </div>
1129            
1130            <div class="grid">
1131                <div class="card">
1132                    <h3>📚 Book Overview</h3>
1133                    <p><strong>Genre:</strong> ${escapeHtml(analysisData.genre)}</p>
1134                    <p><strong>Word Count:</strong> ${analysisData.wordCount.toLocaleString()}</p>
1135                    <p><strong>Chapters:</strong> ${analysisData.chapterCount}</p>
1136                    <p><strong>Reading Time:</strong> ~${analysisData.estimatedReadingTime} minutes</p>
1137                </div>
1138                
1139                <div class="card">
1140                    <h3>📊 Analysis Metrics</h3>
1141                    <p><strong>Complexity Score:</strong> ${analysisData.complexityScore}/10</p>
1142                    <p><strong>Sentiment:</strong> ${escapeHtml(analysisData.sentiment)}</p>
1143                    <p><strong>Target Audience:</strong> ${escapeHtml(analysisData.targetAudience)}</p>
1144                </div>
1145                
1146                <div class="card">
1147                    <h3>🎯 Main Themes</h3>
1148                    <div>
1149                        ${analysisData.themes.map(theme => 
1150                            `<span class="theme-tag">${escapeHtml(theme)}</span>`
1151                        ).join('')}
1152                    </div>
1153                </div>
1154            </div>
1155            
1156            <div class="card">
1157                <h3>📈 Chapter Word Distribution</h3>
1158                <div class="chart-placeholder">Chart visualization would appear here</div>
1159            </div>
1160            
1161            <div class="grid">
1162                ${analysisData.chapterSummaries.map((summary, i) => `
1163                    <div class="card summary-card">
1164                        <h4>Chapter ${i + 1}: ${escapeHtml(summary.title)}</h4>
1165                        <p>${escapeHtml(summary.summary)}</p>
1166                        <small>Word count: ${summary.wordCount}</small>
1167                    </div>
1168                `).join('')}
1169            </div>
1170        </div>
1171    </body>
1172    </html>`;
1173    
1174    const blob = new Blob([html], { type: 'text/html' });
1175    const url = URL.createObjectURL(blob);
1176    const a = document.createElement('a');
1177    a.href = url;
1178    a.download = `${analysisData.title.replace(/[^a-z0-9]/gi, '_')}_interactive_report.html`;
1179    a.click();
1180    URL.revokeObjectURL(url);
1181}
1182
1183/**
1184 * Downloads summary as Word-like document (RTF)
1185 */
1186function downloadSummaryDOCX(analysisData) {
1187    let rtf = `{\\rtf1\\ansi\\deff0
1188{\\fonttbl{\\f0\\froman\\fcharset0 Times New Roman;}}
1189{\\colortbl;\\red0\\green0\\blue0;\\red92\\green107\\blue192;}
1190\\viewkind4\\uc1\\pard\\sa200\\sl276\\slmult1\\f0\\fs24
1191
1192\\pard\\sa200\\sl276\\slmult1\\qc\\b\\fs32\\cf2 ${escapeRtf(analysisData.title)} - Executive Summary\\b0\\fs24\\cf1\\par
1193
1194\\pard\\sa200\\sl276\\slmult1\\b Book Overview\\b0\\par
1195Genre: ${escapeRtf(analysisData.genre)}\\par
1196Word Count: ${analysisData.wordCount.toLocaleString()}\\par
1197Chapters: ${analysisData.chapterCount}\\par
1198Estimated Reading Time: ${analysisData.estimatedReadingTime} minutes\\par
1199Complexity Score: ${analysisData.complexityScore}/10\\par
1200Sentiment: ${escapeRtf(analysisData.sentiment)}\\par
1201Target Audience: ${escapeRtf(analysisData.targetAudience)}\\par\\par
1202
1203\\b Main Themes\\b0\\par`;
1204
1205    analysisData.themes.forEach(theme => {
1206        rtf += `• ${escapeRtf(theme)}\\par`;
1207    });
1208    
1209    rtf += `\\par\\b Chapter Summaries\\b0\\par`;
1210    
1211    analysisData.chapterSummaries.forEach((summary, i) => {
1212        rtf += `\\par\\b Chapter ${i + 1}: ${escapeRtf(summary.title)}\\b0\\par`;
1213        rtf += `${escapeRtf(summary.summary)}\\par`;
1214        rtf += `\\i Word count: ${summary.wordCount}\\i0\\par`;
1215    });
1216    
1217    rtf += `}`;
1218    
1219    const blob = new Blob([rtf], { type: 'application/rtf' });
1220    const url = URL.createObjectURL(blob);
1221    const a = document.createElement('a');
1222    a.href = url;
1223    a.download = `${analysisData.title.replace(/[^a-z0-9]/gi, '_')}_summary.rtf`;
1224    a.click();
1225    URL.revokeObjectURL(url);
1226}
1227
1228/**
1229 * Downloads complete analysis data as JSON
1230 */
1231function downloadAnalysisJSON(analysisData) {
1232    const jsonData = {
1233        ...analysisData,
1234        generatedAt: new Date().toISOString(),
1235        analysisVersion: '1.0'
1236    };
1237    
1238    const blob = new Blob([JSON.stringify(jsonData, null, 2)], { type: 'application/json' });
1239    const url = URL.createObjectURL(blob);
1240    const a = document.createElement('a');
1241    a.href = url;
1242    a.download = `${analysisData.title.replace(/[^a-z0-9]/gi, '_')}_analysis_data.json`;
1243    a.click();
1244    URL.revokeObjectURL(url);
1245}
1246
1247/**
1248 * Handles user export operation (PDF/HTML/RTF/JSON/PPTX).
1249 * Disables UI and shows status during operation, restores on completion.
1250 * @param {string} format - The export format ('pdf', 'html', 'rtf', 'json', 'pptx')
1251 * @param {HTMLElement} popupContainer - Popup DOM node
1252 */
1253async function handleExport(format, popupContainer) {
1254    const statusContainer = popupContainer.querySelector('#export-status');
1255    const statusMessage = popupContainer.querySelector('#export-status-message');
1256    statusContainer.style.display = 'flex';
1257    try {
1258        popupContainer.querySelectorAll('button').forEach(btn => btn.disabled = true);
1259        const bookTitle = document.querySelector('h1').textContent;
1260        const chapters = Array.from(document.querySelectorAll('.chapter'));
1261        if (chapters.length === 0) {
1262            throw new Error("No content found to export. Please generate or load a book first.");
1263        }
1264        const settings = getExportSettings(format);
1265        statusMessage.textContent = `Preparing ${format.toUpperCase()} export...`;
1266        switch (format) {
1267            case 'pdf':
1268                await exportToPDF(bookTitle, chapters, settings, statusMessage);
1269                break;
1270            case 'html':
1271                await exportToHTML(bookTitle, chapters, settings, statusMessage);
1272                break;
1273            case 'rtf':
1274                await exportToRTF(bookTitle, chapters, settings, statusMessage);
1275                break;
1276            case 'docx':
1277                await exportToDOCX(bookTitle, chapters, settings, statusMessage);
1278                break;
1279            case 'json':
1280                await exportToJSON(statusMessage);
1281                break;
1282            case 'pptx':
1283                await exportToPPTX(bookTitle, chapters, settings);
1284                break;
1285            default:
1286                throw new Error(`Unsupported export format: ${format}`);
1287        }
1288        statusMessage.textContent = `${format.toUpperCase()} export completed successfully!`;
1289        setTimeout(() => {
1290            if (popupContainer.isConnected) {
1291                statusContainer.style.display = 'none';
1292                popupContainer.querySelectorAll('button').forEach(btn => btn.disabled = false);
1293            }
1294        }, 1800);
1295    } catch (error) {
1296        console.error(`Export error:`, error);
1297        statusMessage.textContent = `Export failed: ${error.message}`;
1298        statusMessage.style.color = 'red';
1299        setTimeout(() => {
1300            if (popupContainer.isConnected) {
1301                popupContainer.querySelectorAll('button').forEach(btn => btn.disabled = false);
1302            }
1303        }, 2000);
1304    }
1305}
1306
1307/**
1308 * Gathers UI settings for the export dialog.
1309 * @param {string} format
1310 * @returns {Object}
1311 */
1312function getExportSettings(format) {
1313    const settings = {};
1314    const val = (id) => document.getElementById(id);
1315    const checked = (id, def = true) => { const e = val(id); return e ? e.checked : def; };
1316
1317    // Editorial & front-matter package (filled in by the Editorial settings tab).
1318    // The copyright page is NEVER included unless explicitly enabled there.
1319    const editorial = getEditorialSettings();
1320    settings.editorial = editorial;
1321
1322    // Shared Book Structure settings
1323    settings.includeCover = checked('include-cover', true);
1324    settings.includeCopyright = checked('include-copyright', editorial.includeCopyright);
1325    settings.includeToc = checked('include-toc', true);
1326    settings.includePreface = checked('include-preface', true);
1327    settings.includeAboutAuthor = checked('include-about-author', false);
1328    settings.includeImages = true;
1329
1330    // Sources placement (per-format radios were consolidated; default to per-chapter)
1331    const sourcesPlacementRadio = document.querySelector(`input[name="${format}-sources-placement"]:checked`);
1332    settings.sourcesPlacement = sourcesPlacementRadio ? sourcesPlacementRadio.value : 'per-chapter';
1333
1334    if (format === 'pdf') {
1335        settings.pageNumbers = checked('pdf-page-numbers', true);
1336        settings.template = val('pdf-template')?.value || 'professional';
1337        settings.pageSize = val('pdf-size')?.value || 'a4';
1338        settings.includeToc = checked('include-toc', true);
1339    } else if (format === 'html') {
1340        settings.includeCss = checked('html-include-css', true);
1341        settings.template = val('html-template')?.value || 'modern';
1342        settings.includeToc = checked('include-toc', true);
1343    } else if (format === 'rtf') {
1344        settings.template = val('rtf-template')?.value || 'professional';
1345        settings.includeToc = checked('include-toc', true);
1346    } else if (format === 'docx') {
1347        settings.template = val('docx-template')?.value || 'professional';
1348        settings.includeToc = checked('docx-include-toc', true);
1349    } else if (format === 'pptx') {
1350        settings.truncate = checked('pptx-truncate', true) !== false;
1351    }
1352    return settings;
1353}
1354
1355// ============================================================================
1356// PUBLISH-READY STYLE TEMPLATES (shared across PDF, DOCX and RTF exports)
1357// ============================================================================
1358// Each template carries the semantic styling knobs used by every exporter.
1359//   - pdf:   font is fixed to Roboto (the only font bundled with pdfmake), so
1360//            the template differentiates by color, spacing, ornament, alignment,
1361//            drop caps and the cover design.
1362//   - rtf:   `serif` / `sans` map to real system font names.
1363//   - docx:  `serif` / `sans` map to real Word font names in styles.xml.
1364const EXPORT_TEMPLATES = {
1365    // ------------------------------------------------------------ NEW (5) ----
1366    classic: {
1367        label: 'Classic',
1368        group: 'Publish-ready',
1369        serif: 'Garamond',
1370        sans: 'Arial',
1371        bodySize: 11,
1372        lineHeight: 1.44,
1373        align: 'justify',
1374        dropcaps: true,
1375        chapterAlign: 'center',
1376        uppercaseChapters: false,
1377        ornament: true,
1378        cover: 'classic-rule',
1379        colors: {
1380            bg: '#fdfbf6', text: '#2a2620', heading: '#3b2f23',
1381            chapter: '#6b5230', accent: '#9a7b3f', muted: '#8a8378',
1382            rule: '#c9a24f', coverBg: '#f6eeda', coverText: '#4a3a24'
1383        }
1384    },
1385    literary: {
1386        label: 'Literary',
1387        group: 'Publish-ready',
1388        serif: 'Georgia',
1389        sans: 'Verdana',
1390        bodySize: 11.5,
1391        lineHeight: 1.5,
1392        align: 'justify',
1393        dropcaps: true,
1394        chapterAlign: 'center',
1395        uppercaseChapters: true,
1396        ornament: true,
1397        cover: 'literary',
1398        colors: {
1399            bg: '#fcfaf6', text: '#1c1a17', heading: '#141210',
1400            chapter: '#5c4a32', accent: '#7b5826', muted: '#8c8378',
1401            rule: '#b48a4a', coverBg: '#241c12', coverText: '#f3ead9'
1402        }
1403    },
1404    modern: {
1405        label: 'Modern',
1406        group: 'Publish-ready',
1407        serif: 'Helvetica Neue',
1408        sans: 'Helvetica',
1409        bodySize: 10.5,
1410        lineHeight: 1.58,
1411        align: 'left',
1412        dropcaps: false,
1413        chapterAlign: 'left',
1414        uppercaseChapters: true,
1415        ornament: false,
1416        cover: 'modern',
1417        colors: {
1418            bg: '#ffffff', text: '#20232a', heading: '#0d1117',
1419            chapter: '#1f2430', accent: '#0a66c2', muted: '#6b7280',
1420            rule: '#d1d5db', coverBg: '#f6f8fb', coverText: '#111827'
1421        }
1422    },
1423    executive: {
1424        label: 'Executive',
1425        group: 'Publish-ready',
1426        serif: 'Gill Sans',
1427        sans: 'Avenir',
1428        bodySize: 11,
1429        lineHeight: 1.56,
1430        align: 'left',
1431        dropcaps: false,
1432        chapterAlign: 'left',
1433        uppercaseChapters: true,
1434        ornament: false,
1435        cover: 'executive',
1436        colors: {
1437            bg: '#ffffff', text: '#1f2328', heading: '#16181d',
1438            chapter: '#1a1d23', accent: '#bf9b30', muted: '#6f7480',
1439            rule: '#d8c98a', coverBg: '#1b1d22', coverText: '#f5f3ee'
1440        }
1441    },
1442    academic: {
1443        label: 'Academic',
1444        group: 'Publish-ready',
1445        serif: 'Cambria',
1446        sans: 'Calibri',
1447        bodySize: 11,
1448        lineHeight: 1.32,
1449        align: 'justify',
1450        dropcaps: false,
1451        chapterAlign: 'left',
1452        uppercaseChapters: true,
1453        ornament: false,
1454        cover: 'academic',
1455        colors: {
1456            bg: '#ffffff', text: '#191c22', heading: '#10243e',
1457            chapter: '#1f3a5f', accent: '#2f6db0', muted: '#5a6472',
1458            rule: '#b7c2cf', coverBg: '#f0f3f8', coverText: '#10243e'
1459        }
1460    },
1461    // ------------------------------------------------------------- existing ---
1462    professional: {
1463        label: 'Professional',
1464        group: 'Default',
1465        serif: 'Roboto',
1466        sans: 'Roboto',
1467        bodySize: 11,
1468        lineHeight: 1.5,
1469        align: 'justify',
1470        dropcaps: false,
1471        chapterAlign: 'center',
1472        uppercaseChapters: false,
1473        ornament: false,
1474        cover: 'light',
1475        colors: {
1476            bg: '#ffffff', text: '#262626', heading: '#0f2e60',
1477            chapter: '#2b3e64', accent: '#5c6bc0', muted: '#7a7f8a',
1478            rule: '#bfc8d8', coverBg: '#f3f6f9', coverText: '#1f2733'
1479        }
1480    },
1481    book: {
1482        label: 'Book',
1483        group: 'Default',
1484        serif: 'Georgia',
1485        sans: 'Arial',
1486        bodySize: 11.5,
1487        lineHeight: 1.5,
1488        align: 'justify',
1489        dropcaps: true,
1490        chapterAlign: 'center',
1491        uppercaseChapters: false,
1492        ornament: false,
1493        cover: 'book',
1494        colors: {
1495            bg: '#f8f5ec', text: '#262221', heading: '#502015',
1496            chapter: '#614334', accent: '#7e4c1d', muted: '#8a7f6d',
1497            rule: '#c8a24a', coverBg: '#f6f3ea', coverText: '#614334'
1498        }
1499    },
1500    manuscript: {
1501        label: 'Manuscript',
1502        group: 'Default',
1503        serif: 'Courier New',
1504        sans: 'Courier New',
1505        bodySize: 12,
1506        lineHeight: 2,
1507        align: 'left',
1508        dropcaps: false,
1509        chapterAlign: 'left',
1510        uppercaseChapters: false,
1511        ornament: false,
1512        cover: 'light',
1513        colors: {
1514            bg: '#fcfcf8', text: '#2b2b2b', heading: '#212121',
1515            chapter: '#2c2541', accent: '#7f6d51', muted: '#8a8a80',
1516            rule: '#bbbbaa', coverBg: '#f5ede2', coverText: '#49342e'
1517        }
1518    },
1519    minimal: {
1520        label: 'Minimal',
1521        group: 'Default',
1522        serif: 'Helvetica',
1523        sans: 'Helvetica',
1524        bodySize: 11,
1525        lineHeight: 1.6,
1526        align: 'left',
1527        dropcaps: false,
1528        chapterAlign: 'left',
1529        uppercaseChapters: true,
1530        ornament: false,
1531        cover: 'light',
1532        colors: {
1533            bg: '#ffffff', text: '#1f1f1f', heading: '#111111',
1534            chapter: '#333333', accent: '#555555', muted: '#767676',
1535            rule: '#dddddd', coverBg: '#ffffff', coverText: '#111111'
1536        }
1537    }
1538};
1539
1540const PDF_FONT = 'Roboto'; // only font bundled with the pdfmake vfs
1541
1542/**
1543 * Resolves a template id to its config, falling back to 'professional'.
1544 * @param {string} template
1545 * @returns {object} the template definition
1546 */
1547function getTemplateDef(template) {
1548    return EXPORT_TEMPLATES[template] || EXPORT_TEMPLATES.professional;
1549}
1550
1551/** Ordered list of template ids shown in the export pickers. */
1552const EXPORT_TEMPLATE_IDS = [
1553    'classic', 'literary', 'modern', 'executive', 'academic',
1554    'professional', 'book', 'manuscript', 'minimal'
1555];
1556
1557/**
1558 * Renders the <option> list for a template <select>.
1559 * @param {string} current the currently-selected template id
1560 * @returns {string} html fragment
1561 */
1562function templateOptionsHTML(current) {
1563    let lastGroup = '';
1564    let html = '';
1565    for (const id of EXPORT_TEMPLATE_IDS) {
1566        const t = EXPORT_TEMPLATES[id];
1567        if (t.group !== lastGroup) {
1568            if (lastGroup) html += '</optgroup>';
1569            html += `<optgroup label="${escapeHtml(t.group)}">`;
1570            lastGroup = t.group;
1571        }
1572        html += `<option value="${id}"${id === current ? ' selected' : ''}>${escapeHtml(t.label)}</option>`;
1573    }
1574    return html + '</optgroup>';
1575}
1576
1577// --- ULTIMATE PDF EXPORT (pdfmake: LaTeX-like structured typesetting) ---
1578// Renders the whole book as a paged document: cover (with generated image),
1579// editorial page, copyright page, automatic TOC with page numbers, and every
1580// chapter with all added elements. Text wraps and images scale automatically
1581// so nothing ever overflows the page.
1582
1583// Splits a chapter label "1. Title" (or the on-screen "1.<br>Title") into a
1584// number and a title so the exported headline keeps the requested line break.
1585function splitChapterLabel(text) {
1586    const m = String(text || '').trim().match(/^(\d+)\s*[.:)]?\s*(.*)$/s);
1587    if (m && m[1]) return { num: m[1], title: (m[2] || '').trim() };
1588    return { num: '', title: String(text || '').trim() };
1589}
1590
1591async function importPdfMake() {
1592    if (window.pdfMake && window.pdfMake.vfs) return window.pdfMake;
1593    const load = (src) => new Promise((resolve, reject) => {
1594        const id = 'pm-' + Math.abs(src.split('').reduce((a, c) => a + c.charCodeAt(0), 0));
1595        if (document.getElementById(id)) return resolve();
1596        const s = document.createElement('script');
1597        s.id = id;
1598        s.src = src;
1599        s.onload = () => setTimeout(resolve, 60);
1600        s.onerror = () => reject(new Error('CDN load failed: ' + src));
1601        document.head.appendChild(s);
1602    });
1603    try {
1604        if (!(window.pdfMake && window.pdfMake.vfs)) {
1605            if (!window.pdfMake) await load('https://cdn.jsdelivr.net/npm/[email protected]/build/pdfmake.min.js');
1606            if (!(window.pdfMake && window.pdfMake.vfs)) await load('https://cdn.jsdelivr.net/npm/[email protected]/build/vfs_fonts.js');
1607        }
1608    } catch (e) {
1609        throw new Error('Failed to load pdfmake from CDN.');
1610    }
1611    if (!window.pdfMake || !window.pdfMake.vfs) throw new Error('pdfmake unavailable');
1612    if (!window.pdfMake.fonts) {
1613        window.pdfMake.fonts = {
1614            Roboto: {
1615                normal: 'Roboto-Regular.ttf',
1616                bold: 'Roboto-Medium.ttf',
1617                italics: 'Roboto-Italic.ttf',
1618                bolditalics: 'Roboto-MediumItalic.ttf'
1619            }
1620        };
1621    }
1622    return window.pdfMake;
1623}
1624
1625async function loadImageDataURL(src) {
1626    if (!src) return null;
1627    if (/^data:/i.test(src)) return src;
1628    try {
1629        const resp = await fetch(src, { mode: 'cors' });
1630        if (!resp.ok) return null;
1631        const blob = await resp.blob();
1632        return await new Promise((resolve) => {
1633            const r = new FileReader();
1634            r.onloadend = () => resolve(r.result);
1635            r.onerror = () => resolve(null);
1636            r.readAsDataURL(blob);
1637        });
1638    } catch (_) { return null; }
1639}
1640
1641function getCoverImageUrlFromBody() {
1642    const bg = document.body ? document.body.style.backgroundImage : '';
1643    if (bg && bg !== 'none') {
1644        const m = bg.match(/url\("?([^"]+?)"?\)/i);
1645        if (m && m[1]) return m[1];
1646    }
1647    return '';
1648}
1649
1650async function exportToPDF(bookTitle, chapters, settings, statusMessage) {
1651    const pdfMake = await importPdfMake();
1652    const tmpl = getTemplateDef((settings && settings.template));
1653    const C = tmpl.colors;
1654    if (statusMessage) statusMessage.textContent = `Typesetting the book\u00a0(${tmpl.label} template)\u2026`;
1655
1656    const ed = (settings && settings.editorial) || getEditorialSettings();
1657    let authorText = (ed.author || '').trim();
1658    if (!authorText) {
1659        try { authorText = await getExportAuthor(); } catch (_) { authorText = ''; }
1660    }
1661
1662    const contentW = 170; // usable width on an A4-style page (mm)
1663
1664    // --- Load the generated cover image (from the current book background) ---
1665    const coverDataUrl = await loadImageDataURL(getCoverImageUrlFromBody());
1666
1667    // --- Build front matter + chapters ---
1668    const docDefinition = {
1669        pageSize: (settings.pageSize || 'a4').toUpperCase() === 'LETTER' ? 'LETTER' :
1670                  (settings.pageSize || 'a4').toUpperCase() === 'A5' ? 'A5' :
1671                  (settings.pageSize || 'a4').toUpperCase() === 'B5' ? 'B5' : 'A4',
1672        pageMargins: [40, 46, 40, 46],
1673        defaultStyle: { font: PDF_FONT, fontSize: tmpl.bodySize, lineHeight: tmpl.lineHeight, color: C.text },
1674        info: { title: bookTitle, author: authorText || undefined },
1675        styles: {
1676            cover: { fontSize: 34, bold: true, alignment: 'center', color: C.heading, margin: [0, 0, 0, 6] },
1677            coverSub: { fontSize: 15, italics: true, alignment: 'center', color: C.muted, margin: [0, 0, 0, 20] },
1678            coverMeta: { fontSize: 12, alignment: 'center', color: C.text, margin: [0, 2, 0, 2] },
1679            coverImprint: { fontSize: 10, alignment: 'center', color: C.muted, margin: [0, 18, 0, 0] },
1680            frontTitle: { fontSize: 16, bold: true, alignment: 'center', color: C.heading, margin: [0, 0, 0, 10], characterSpacing: 0.3 },
1681            para: { fontSize: tmpl.bodySize, lineHeight: tmpl.lineHeight, alignment: tmpl.align, margin: [0, 6, 0, 6] },
1682            frontBody: { fontSize: Math.max(tmpl.bodySize - 0.5, 9.5), lineHeight: tmpl.lineHeight, alignment: 'center', margin: [0, 6, 0, 6], color: C.text },
1683            dedication: { fontSize: 13, italics: true, alignment: 'center', color: C.text, lineHeight: 1.6, margin: [0, 6, 0, 6] },
1684            epigraph: { fontSize: 12, italics: true, alignment: 'center', color: C.text, lineHeight: 1.6, margin: [0, 6, 0, 6] },
1685            epigraphSource: { fontSize: 10, alignment: 'right', color: C.muted, margin: [20, 0, 60, 0] },
1686            tocTitle: { fontSize: 15, bold: true, alignment: 'center', color: C.heading, margin: [0, 0, 0, 10] },
1687            tocEntry: { fontSize: 9, color: C.muted, margin: [8, 0, 8, 4] },
1688            small: { fontSize: 8.5, color: C.muted, alignment: 'center', lineHeight: 1.35 },
1689            sourceHeading: { fontSize: 13, bold: true, color: C.heading, margin: [0, 14, 0, 4] }
1690        },
1691        content: [],
1692        header: (currentPage) => {
1693            if (currentPage <= 1) return null;
1694            return {
1695                columns: [
1696                    { text: bookTitle, fontSize: 8.5, color: C.muted, alignment: 'right', margin: [40, 16, 0, 0] }
1697                ]
1698            };
1699        },
1700        footer: (currentPage) => {
1701            if (currentPage <= 1) return null;
1702            return {
1703                columns: [
1704                    { text: String(currentPage), fontSize: 9, color: C.muted, alignment: 'center', margin: [0, 8, 0, 0] }
1705                ]
1706            };
1707        }
1708    };
1709
1710    const content = docDefinition.content;
1711    let first = true;
1712
1713    // Page dimensions (mm) for the full-bleed background + cover box.
1714    const PAGE_DIMS = { A4: [210, 297], LETTER: [216, 279], A5: [148, 210], B5: [176, 250] };
1715    const [pageWmm, pageHmm] = PAGE_DIMS[docDefinition.pageSize] || [210, 297];
1716
1717    // ===== Decorative rule (uses the template accent) =====
1718    const rule = (width, marginTop) => ({
1719        canvas: [{ type: 'line', x1: 0, y1: 0, x2: width, y2: 0, lineWidth: 0.7, lineColor: C.rule }
1719],
1720        alignment: 'center',
1721        margin: [0, marginTop, 0, 0]
1722    });
1723
1724    const frontPage = (contentItems) => {
1725        if (!first) content.push({ text: '', pageBreak: 'before' });
1726        first = false;
1727        contentItems.forEach((it) => content.push(it));
1728    };
1729
1730    // ===== 1) HALF-TITLE PAGE (optional) =====
1731    if (ed.showHalfTitle !== false) {
1732        if (first) { content.push({ text: '', margin: [0, 140, 0, 0] }); first = false; }
1733        else content.push({ text: '', pageBreak: 'before' });
1734        content.push({ text: bookTitle, fontSize: 22, bold: true, alignment: 'center', color: C.heading });
1735    }
1736
1737    // ===== 2) TITLE / COVER PAGE =====
1738    const box = (textContent) => ({
1739        table: {
1740            widths: ['100%'],
1741            body: [[
1742                {
1743                    stack: textContent,
1744                    fillColor: C.coverBg,
1745                    color: C.coverText,
1746                    alignment: 'center',
1747                    margin: [24, 24, 24, 24]
1748                }
1749            ]]
1750        },
1751        layout: 'noBorders',
1752        width: Math.min(158, pageWmm - 24),
1753        absolutePosition: { x: (pageWmm - Math.min(158, pageWmm - 24)) / 2, y: Math.round(pageHmm * 0.38) }
1754    });
1755
1756    const buildTitleStack = () => {
1757        const s = [];
1758        if (tmpl.ornament) s.push(rule(44, 0));
1759        s.push({ text: bookTitle, fontSize: 30, bold: true, color: C.coverText, alignment: 'center', margin: [0, 10, 0, 8] });
1760        if (ed.showSubtitle && ed.subtitle) s.push({ text: ed.subtitle, fontSize: 15, italics: true, color: C.muted === C.coverText ? '#9f9f9f' : C.coverText, opacity: 0.85, alignment: 'center', margin: [0, 0, 0, 14] });
1761        if (tmpl.ornament) s.push(rule(28, 0));
1762        if (ed.showAuthor && authorText) s.push({ text: authorText, fontSize: 13, color: C.coverText, alignment: 'center', margin: [0, 14, 0, 10] });
1763        if (ed.showImprint && ed.imprint) s.push({ text: ed.imprint, fontSize: 11, color: C.coverText, opacity: 0.75, alignment: 'center', margin: [0, 0, 0, 4] });
1764        if (ed.showEdition && ed.edition) s.push({ text: ed.edition, fontSize: 11, color: C.coverText, opacity: 0.75, alignment: 'center', margin: [0, 0, 0, 4] });
1765        s.push({ text: String(ed.copyrightYear || new Date().getFullYear()), fontSize: 11, color: C.coverText, opacity: 0.7, alignment: 'center', margin: [0, 8, 0, 0] });
1766        return s;
1767    };
1768
1769    if (coverDataUrl) {
1770        frontPage([
1771            { image: coverDataUrl, absolutePosition: { x: 0, y: 0 }, width: pageWmm, height: pageHmm },
1772            box(buildTitleStack())
1773        ]);
1774    } else {
1775        const fallback = [];
1776        if (tmpl.ornament) fallback.push(rule(60, 0));
1777        fallback.push({ text: bookTitle, style: 'cover', margin: [0, 10, 0, 0] });
1778        if (ed.showSubtitle && ed.subtitle) fallback.push({ text: ed.subtitle, style: 'coverSub' });
1779        else fallback.push({ text: '', margin: [0, 0, 0, 20] });
1780        if (ed.showAuthor && authorText) fallback.push({ text: authorText, style: 'coverMeta', margin: [0, 12, 0, 0] });
1781        if (ed.showImprint && ed.imprint) fallback.push({ text: ed.imprint, style: 'coverImprint' });
1782        if (ed.showEdition && ed.edition) fallback.push({ text: ed.edition, style: 'coverImprint' });
1783        fallback.push({ text: String(ed.copyrightYear || new Date().getFullYear()), style: 'coverImprint' });
1784        frontPage(fallback);
1785    }
1786
1787    // ===== 3) DEDICATION (optional) =====
1788    if (ed.showDedication && ed.dedication) {
1789        frontPage([
1790            { text: '', margin: [0, 120, 0, 0] },
1791            { text: ed.dedication, style: 'dedication' }
1792        ]);
1793    }
1794
1795    // ===== 4) EPIGRAPH (optional) =====
1796    if (ed.showEpigraph && ed.epigraph) {
1797        frontPage([
1798            { text: '', margin: [0, 130, 0, 0] },
1799            { text: ed.epigraph, style: 'epigraph' },
1800            ...(ed.epigraphSource ? [{ text: '\u2014 ' + ed.epigraphSource, style: 'epigraphSource', margin: [0, 10, 0, 0] }] : [])
1801        ]);
1802    }
1803
1804    // ===== 5) FOREWORD (optional) =====
1805    if (ed.showForeword && ed.foreword) {
1806        frontPage([
1807            { text: 'Foreword', style: 'frontTitle' },
1808            ...ed.foreword.split(/\n{2,}
1808/).filter(Boolean).map((p) => ({ text: p.trim(), style: 'para' }))
1809        ]);
1810    }
1811
1812    // ===== 6) ACKNOWLEDGMENTS (optional) =====
1813    if (ed.showAcknowledgments && ed.acknowledgments) {
1814        frontPage([
1815            { text: 'Acknowledgments', style: 'frontTitle' },
1816            ...ed.acknowledgments.split(/\n{2,}/).filter(Boolean).map((p) => ({ text: p.trim(), style: 'para' }))
1817        ]);
1818    }
1819
1820    // ===== 7) EDITORIAL / INTRODUCTION (in front of the TOC) =====
1821    if (ed.includeEditorial && ed.editorialText) {
1822        frontPage([
1823            { text: 'Editorial', style: 'frontTitle' },
1824            ...ed.editorialText.split(/\n{2,}/).filter(Boolean).map((p) => ({ text: p.trim(), style: 'para' }))
1825        ]);
1826    }
1827
1828    // ===== 8) COPYRIGHT & LEGAL PAGE (ONLY when explicitly enabled) =====
1829    if (settings.includeCopyright) {
1830        const holder = (ed.copyrightHolder || authorText || 'The Author').trim();
1831        const year = ed.copyrightYear || new Date().getFullYear();
1832        const rights = ed.rightsText || 'All rights reserved. No part of this publication may be reproduced, distributed, or transmitted in any form without the prior written permission of the publisher.';
1833        const lines = [];
1834        lines.push(bookTitle);
1835        if (authorText) lines.push('Copyright \u00A9 ' + year + ' ' + holder);
1836        lines.push(rights);
1837        if (ed.showEdition && ed.edition) lines.push(ed.edition);
1838        if (ed.showIsbn && ed.isbn) lines.push('ISBN ' + ed.isbn);
1839        if (ed.showCredits && ed.credits) lines.push(ed.credits);
1840        frontPage([
1841            { text: '', margin: [0, 80, 0, 0] },
1842            ...lines.map((l, i) => ({
1843                text: l,
1844                style: 'small',
1845                margin: [0, i === 0 ? 0 : 10, 0, 0],
1846                bold: i === 0
1847            }))
1848        ]);
1849    }
1850
1851    // ===== 9) TABLE OF CONTENTS (compact) =====
1852    if (settings.includeToc) {
1853        frontPage([
1854            {
1855                toc: {
1856                    title: { text: getTocLabelForExport(), style: 'tocTitle' },
1857                    textStyle: { fontSize: 11, color: C.text, lineHeight: 1.3 },
1858                    contentStyle: { margin: [0, 2, 0, 2] }
1859                }
1860            }
1861        ]);
1862    }
1863
1864    // ===== 10) PREFACE (AI-written, optional) =====
1865    let fm = { preface: '', aboutAuthor: '' };
1866    if (settings.includePreface || settings.includeAboutAuthor) {
1867        try { fm = await generateFrontMatter(bookTitle, chapters); } catch (_) {}
1868    }
1869    if (settings.includePreface && fm.preface) {
1870        frontPage([
1871            { text: getPrefaceLabel(), style: 'frontTitle' },
1872            ...fm.preface.split(/\n{2,}/).filter(Boolean).map((p) => ({ text: p.trim(), style: 'para' }))
1873        ]);
1874    }
1875
1876    // ===== CHAPTERS (all chapters + all added elements, wrapped to the page) =====
1877    const allSources = [];
1878    for (let idx = 0; idx < chapters.length; idx++) {
1879        const chapter = chapters[idx];
1880        const titleEl = chapter.querySelector('h2');
1881        const label = splitChapterLabel(titleEl ? titleEl.textContent : `Chapter ${idx + 1}`);
1882        const chapterNum = label.num || String(idx + 1);
1883        let chapterTitle = label.title || label.num || `Chapter ${idx + 1}`;
1884        if (tmpl.uppercaseChapters) chapterTitle = String(chapterTitle).toUpperCase();
1885
1886        // Chapter opener with optional decorative rule above the title.
1887        const opening = [];
1888        const titleNode = {
1889            text: `${chapterNum}. ${chapterTitle}`,
1890            fontSize: 22,
1891            bold: true,
1892            color: C.chapter,
1893            alignment: tmpl.chapterAlign,
1894            margin: [0, 6, 0, 12],
1895            tocItem: true,
1896            tocStyle: { fontSize: 11, color: C.muted },
1897            tocMargin: [0, 2, 0, 2]
1898        };
1899        if (tmpl.ornament) {
1900            opening.push({ canvas: [{ type: 'line', x1: 0, y1: 0, x2: 46, y2: 0, lineWidth: 0.6, lineColor: C.rule }], alignment: 'center', pageBreak: 'before', margin: [0, 0, 0, 12] });
1901            opening.push(titleNode);
1902        } else {
1903            titleNode.pageBreak = 'before';
1904            opening.push(titleNode);
1905        }
1906        opening.forEach((n) => content.push(n));
1907
1908        // Images inside the chapter
1909        if (settings.includeImages !== false) {
1910            const imgs = Array.from(chapter.querySelectorAll('.chapter-content .image-container img'));
1911            for (const img of imgs) {
1912                const src = (img.currentSrc || img.src || '');
1913                const dataUrl = src.startsWith('data:') ? src : await loadImageDataURL(src);
1914                if (dataUrl) {
1915                    content.push({ image: dataUrl, width: Math.min(130, contentW), alignment: 'center', margin: [0, 10, 0, 10] });
1916                }
1917            }
1918        }
1919
1920        // Body paragraphs (auto-wrap + sizing). Optionally drop-cap the first.
1921        const paras = chapter.querySelectorAll('.chapter-content > p');
1922        let firstPara = true;
1923        for (const p of paras) {
1924            const text = cleanParagraphText(p);
1925            if (!text) continue;
1926            if (firstPara && tmpl.dropcaps) {
1927                firstPara = false;
1928                const ch = Array.from(text)[0];
1929                const rest = text.slice(ch.length);
1930                content.push({
1931                    columns: [
1932                        { text: ch, fontSize: 40, bold: true, color: C.accent, width: 18, margin: [0, 4, 0, 0], alignment: 'left' },
1933                        { text: rest, style: 'para' }
1934                    ],
1935                    columnGap: 5,
1936                    margin: [0, 6, 0, 6]
1937                });
1938            } else {
1939                content.push({ text, style: 'para' });
1940            }
1941        }
1942
1943        // Per-chapter sources
1944        if (settings.sourcesPlacement === 'per-chapter') {
1945            const srcBox = chapter.querySelector('.chapter-content .chapter-sources-container');
1946            const items = srcBox ? Array.from(srcBox.querySelectorAll('.chapter-source-link')) : [];
1947            if (items.length) {
1948                content.push({ text: 'Sources & Links', style: 'sourceHeading' });
1949                items.forEach((li) => {
1950                    content.push({ text: sourceLine(li), fontSize: 9, color: C.muted, margin: [0, 2, 0, 2] });
1951                });
1952            }
1953        } else if (settings.sourcesPlacement === 'end-of-book') {
1954            const srcBox = chapter.querySelector('.chapter-content .chapter-sources-container');
1955            const items = srcBox ? Array.from(srcBox.querySelectorAll('.chapter-source-link')) : [];
1956            if (items.length) allSources.push({ title: `${chapterNum}. ${chapterTitle}`, items });
1957        }
1958    }
1959
1960    // ===== END-OF-BOOK SOURCES =====
1961    if (settings.sourcesPlacement === 'end-of-book' && allSources.length) {
1962        content.push({ text: 'Sources & References', style: 'frontTitle', pageBreak: 'before' });
1963        allSources.forEach((cs) => {
1964            content.push({ text: cs.title, bold: true, color: C.heading, margin: [0, 10, 0, 4] });
1965            cs.items.forEach((li) => content.push({ text: sourceLine(li), fontSize: 9, color: C.muted, margin: [0, 2, 0, 2] }));
1966        });
1967    }
1968
1969    // ===== ABOUT THE AUTHOR (back matter, only when the checkbox is on) =====
1970    if (settings.includeAboutAuthor && fm.aboutAuthor) {
1971        content.push({ text: getAboutAuthorLabel(), style: 'frontTitle', pageBreak: 'before' });
1972        fm.aboutAuthor.split(/\n{2,}/).filter(Boolean).forEach((p) => content.push({ text: p.trim(), style: 'para' }));
1973    }
1974
1975    const safeName = bookTitle.replace(/[^a-z0-9]/gi, '_').toLowerCase();
1976    const pdfDoc = pdfMake.createPdf(docDefinition);
1977    pdfDoc.download(`${safeName}.pdf`);
1978}
1979function cleanParagraphText(p) {
1980    return (p.textContent || '').trim();
1981}
1982
1983function sourceLine(li) {
1984    const a = li.querySelector('a');
1985    const title = a ? a.textContent.trim() : '';
1986    const url = a ? a.href : '';
1987    let line = title;
1988    if (url) line += ` — ${url}`;
1989    return line || 'Source';
1990}
1991
1992/**
1993 * Exports the book to HTML format.
1994 * @param {string} bookTitle - The title of the book
1995 * @param {Array<HTMLElement>} chapters - Array of chapter elements
1996 * @param {Object} settings - Export settings
1997 */
1998async function exportToHTML(bookTitle, chapters, settings, statusMessage) { 
1999    statusMessage.textContent = 'Writing semantic HTML with front matter...';
2000    const fm = await generateFrontMatter(bookTitle, chapters);
2001    const ed = (settings && settings.editorial) || getEditorialSettings();
2002    let authorText = (ed.author || '').trim();
2003    if (!authorText) authorText = await getExportAuthor();
2004    let html = `<!DOCTYPE html>
2005<html lang="en">
2006<head>
2007    <meta charset="UTF-8">
2008    <meta name="viewport" content="width=device-width, initial-scale=1.0">
2009    <meta name="author" content="${escapeHtml(authorText)}">
2010    <title>${escapeHtml(bookTitle)}</title>`;
2011    
2012    if (settings.includeCss) {
2013        html += `
2014    <style>
2015        :root {
2016            --text-color: #333;
2017            --bg-color: #fff;
2018            --heading-color: #222;
2019            --link-color: #0066cc;
2020            --border-color: #ddd;
2021            --cover-bg: ${settings.template === 'classic' ? '#f9f7f1' : 
2022                        settings.template === 'ebook' ? '#202020' : 
2023                        settings.template === 'minimal' ? '#ffffff' : '#f0f4f8'};
2024            --cover-text: ${settings.template === 'ebook' ? '#ffffff' : '#000000'};
2025            --font-family: ${settings.template === 'classic' ? "'Georgia', serif" : 
2026                          settings.template === 'ebook' ? "'Segoe UI', sans-serif" :
2027                          settings.template === 'minimal' ? "'Arial', sans-serif" : "'Noto Serif', 'Georgia', serif"};
2028            --line-height: ${settings.template === 'ebook' ? '1.8' : '1.6'};
2029            --max-width: ${settings.template === 'minimal' ? '650px' : '800px'};
2030        }
2031        
2032        html, body {
2033            margin: 0;
2034            padding: 0;
2035            font-family: var(--font-family);
2036            line-height: var(--line-height);
2037            color: var(--text-color);
2038            background-color: var(--bg-color);
2039        }
2040        
2041        .book-container {
2042            max-width: var(--max-width);
2043            margin: 0 auto;
2044            padding: 2rem;
2045        }
2046        
2047        h1, h2, h3 {
2048            color: var(--heading-color);
2049            line-height: 1.3;
2050        }
2051        
2052        h1 {
2053            font-size: 2.2rem;
2054            margin-bottom: 2rem;
2055            text-align: center;
2056        }
2057        
2058        h2 {
2059            font-size: 1.8rem;
2060            margin-top: 3rem;
2061            margin-bottom: 1.5rem;
2062            ${settings.template === 'classic' ? 'border-bottom: 1px solid var(--border-color);' : ''}
2063            ${settings.template === 'classic' ? 'padding-bottom: 0.5rem;' : ''}
2064        }
2065        
2066        p {
2067            margin-bottom: 1.2rem;
2068            ${settings.template === 'classic' ? 'text-align: justify;' : ''}
2069            ${settings.template === 'ebook' ? 'font-size: 1.1rem;' : ''}
2070        }
2071        
2072        .cover-page {
2073            height: 90vh;
2074            display: flex;
2075            flex-direction: column;
2076            justify-content: center;
2077            align-items: center;
2078            text-align: center;
2079            background-color: var(--cover-bg);
2080            color: var(--cover-text);
2081            padding: 2rem;
2082            margin-bottom: 2rem;
2083            ${settings.template === 'modern' ? 'border-radius: 8px;' : ''}
2084            ${settings.template === 'ebook' ? 'box-shadow: 0 4px 20px rgba(0,0,0,0.3);' : ''}
2085        }
2086        
2087        .cover-page h1 {
2088            font-size: 3rem;
2089            margin-bottom: 2rem;
2090        }
2091        
2092        .cover-page .author {
2093            font-size: 1.3rem;
2094            margin-bottom: 1rem;
2095            font-style: italic;
2096        }
2097        
2098        .cover-page .date {
2099            font-size: 1rem;
2100        }
2101        
2102        .toc {
2103            margin: 2rem 0 3rem;
2104            ${settings.template === 'modern' ? 'background-color: #f7f9fc;' : ''}
2105            ${settings.template === 'modern' ? 'padding: 1.5rem;' : ''}
2106            ${settings.template === 'modern' ? 'border-radius: 8px;' : ''}
2107        }
2108        
2109        .toc h2 {
2110            margin-top: 0;
2111            text-align: center;
2112        }
2113        
2114        .toc-entry {
2115            display: flex;
2116            align-items: center;
2117            margin-bottom: 0.8rem;
2118        }
2119        
2120        .toc-number {
2121            margin-right: 0.8rem;
2122            font-weight: bold;
2123        }
2124        
2125        .toc-title {
2126            flex-grow: 1;
2127        }
2128        
2129        .toc-dots {
2130            flex-grow: 1;
2131            border-bottom: 1px dotted #ccc;
2132            margin: 0 0.5rem;
2133        }
2134        
2135        .toc a {
2136            color: var(--link-color);
2137            text-decoration: none;
2138        }
2139        
2140        .toc a:hover {
2141            text-decoration: underline;
2142        }
2143        
2144        .chapter-image {
2145            max-width: 100%;
2146            height: auto;
2147            margin: 1.5rem auto;
2148            display: block;
2149            ${settings.template === 'modern' ? 'border-radius: 4px;' : ''}
2150            ${settings.template === 'ebook' ? 'box-shadow: 0 2px 10px rgba(0,0,0,0.1);' : ''}
2151        }
2152        
2153        .chapter-image.round-cutout {
2154            border-radius: 50%;
2155            max-width: 300px;
2156        }
2157        
2158        @media print {
2159            .cover-page {
2160                height: 100vh;
2161                page-break-after: always;
2162            }
2163            
2164            .toc {
2165                page-break-after: always;
2166            }
2167            
2168            h2 {
2169                page-break-before: always;
2170            }
2171            
2172            h2, h3 {
2173                page-break-after: avoid;
2174            }
2175            
2176            p {
2177                widows: 3;
2178                orphans: 3;
2179            }
2180            
2181            img {
2182                page-break-inside: avoid;
2183            }
2184        }
2185        
2186        /* Style for sources/links section exported */
2187        .chapter-sources-container {
2188            margin-top:28px;
2189            border-top:2px dotted #bbb;
2190            padding:10px 0 0 0;
2191        }
2192        .chapter-sources-container > div:first-child {
2193            color:#5c6bc0;
2194            font-weight:bold;
2195            margin-bottom:7px;
2196            font-size:1.13em;
2197        }
2198        .chapter-source-link {
2199            margin-bottom:7px;
2200        }
2201        .chapter-source-link a {
2202            font-weight:bold;
2203            color:#2563eb;
2204            text-decoration:underline;
2205            word-break:break-all;
2206        }
2207        .chapter-source-link span {
2208            font-size:0.92em;
2209            color:#777;
2210            display:block;
2211            margin-top:2px;
2212        }
2213        /* Styles for end-of-book sources */
2214        .book-sources-end {
2215            margin-top: 3rem;
2216            padding-top: 2rem;
2217            border-top: 2px solid var(--border-color);
2218        }
2219        .book-sources-end h2 {
2220            text-align: center;
2221            margin-bottom: 0.5rem;
2222        }
2223        .book-sources-end .chapter-source-link { /* Reuse existing style */
2224            margin-bottom: 1rem;
2225        }
2226        .half-title, .copyright-page {
2227            text-align: center;
2228            page-break-before: always;
2229        }
2230        .copyright-page small {
2231            color: #777;
2232        }
2233        .front-page { text-align:center; margin: 4rem 0; }
2234        .front-page p { margin: 0.5rem 0; text-align: justify; }
2235        .front-page .dedication, .front-page blockquote {
2236            font-style: italic; margin: 6rem auto 0; max-width: 60%; text-align: center;
2237        }
2238        .front-page .epigraph-source { text-align: right; font-style: normal; color: #888; margin-top: 0.5rem; }
2239        .imprint { color: #777; margin-top: 0.25rem; }
2240        .author-credit { font-style: italic; color: #555; }
2241        @media (prefers-color-scheme: dark) {
2242            :root { --text-color: #cfd3dc; --bg-color: #1c1e26; --heading-color: #e8eaf2; --link-color: #7aa2ff; --border-color: #33373f; }
2243            ${settings.template === 'modern' ? '.toc { background-color: #23262f; }' : ''}
2244            .chapter-source-link span, .copyright-page small, .author-credit { color: #9aa0ad; }
2245        }
2246    </style>`;
2247    }
2248    
2249    html += `
2250</head>
2251<body>
2252    <div class="book-container">`;
2253    
2254    // Add cover page / title page
2255    if (settings.includeCover) {
2256        html += `
2257        <section class="cover-page" role="banner">
2258            <h1>${escapeHtml(bookTitle)}</h1>
2259            ${ed.showSubtitle && ed.subtitle ? `<p class="subtitle">${escapeHtml(ed.subtitle)}</p>` : (fm.subtitle ? `<p class="subtitle">${escapeHtml(fm.subtitle)}</p>` : '')}
2260            ${ed.showImprint && ed.imprint ? `<p class="imprint">${escapeHtml(ed.imprint)}</p>` : ''}
2261            ${ed.showEdition && ed.edition ? `<p class="imprint">${escapeHtml(ed.edition)}</p>` : ''}
2262            ${ed.showAuthor && authorText ? `<p class="author-credit">${escapeHtml(authorText)}</p>` : ''}
2263            <p class="date">${new Date().toLocaleDateString()}</p>
2264        </section>`;
2265    }
2266
2267    // Dedication
2268    if (ed.showDedication && ed.dedication) {
2269        html += `
2270        <section class="front-page" id="dedication">
2271            <p class="dedication">${escapeHtml(ed.dedication)}</p>
2272        </section>`;
2273    }
2274    // Epigraph
2275    if (ed.showEpigraph && ed.epigraph) {
2276        html += `
2277        <section class="front-page" id="epigraph">
2278            <blockquote>${escapeHtml(ed.epigraph)}</blockquote>
2279            ${ed.epigraphSource ? `<p class="epigraph-source">— ${escapeHtml(ed.epigraphSource)}</p>` : ''}
2280        </section>`;
2281    }
2282    // Foreword
2283    if (ed.showForeword && ed.foreword) {
2284        html += `
2285        <section class="front-page" id="foreword">
2286            <h2>Foreword</h2>
2287            ${ed.foreword.split(/\n{2,}/).filter(Boolean).map(p => `<p>${escapeHtml(p)}</p>`).join('')}
2288        </section>`;
2289    }
2290    // Acknowledgments
2291    if (ed.showAcknowledgments && ed.acknowledgments) {
2292        html += `
2293        <section class="front-page" id="acknowledgments">
2294            <h2>Acknowledgments</h2>
2295            ${ed.acknowledgments.split(/\n{2,}/).filter(Boolean).map(p => `<p>${escapeHtml(p)}</p>`).join('')}
2296        </section>`;
2297    }
2298
2299    // Copyright page (ONLY when explicitly enabled in the Editorial tab)
2300    if (settings.includeCopyright) {
2301        const holder = (ed.copyrightHolder || authorText || 'The Author').trim();
2302        const year = ed.copyrightYear || new Date().getFullYear();
2303        const rights = ed.rightsText || 'All rights reserved. No part of this publication may be reproduced, distributed, or transmitted in any form without the prior written permission of the publisher.';
2304        html += `
2305        <section class="copyright-page" role="contentinfo">
2306            <p><strong>${escapeHtml(bookTitle)}</strong></p>
2307            ${authorText ? `<p class="author-credit">${escapeHtml(authorText)}</p>` : ''}
2308            <small>
2308Copyright &copy; ${year} ${escapeHtml(holder)}</small><br>
2309            <small>${escapeHtml(rights)}</small>
2310            ${ed.showEdition && ed.edition ? `<br><small>${escapeHtml(ed.edition)}</small>` : ''}
2311            ${ed.showIsbn && ed.isbn ? `<br><small>ISBN ${escapeHtml(ed.isbn)}</small>` : ''}
2312            ${ed.showCredits && ed.credits ? `<br><small>${escapeHtml(ed.credits)}</small>` : ''}
2313        </section>`;
2314    }
2315
2316    // Editorial / abstract page (in front of the Table of Contents)
2317    if (ed.includeEditorial && ed.editorialText) {
2318        html += `
2319        <section class="front-page" id="editorial">
2320            <h2>Editorial</h2>
2321            ${ed.editorialText.split(/\n{2,}/).filter(Boolean).map(p => `<p>${escapeHtml(p)}</p>`).join('')}
2322        </section>`;
2323    }
2324    
2325    // Preface
2326    if (settings.includePreface) {
2327        const prefacePars = fm.preface.split(/\n{2,}/).filter(Boolean);
2328        html += `
2329        <section class="front-page" id="preface">
2330            <h2>${escapeHtml(getPrefaceLabel())}</h2>
2331            ${prefacePars.map(p => `<p>${escapeHtml(p)}</p>`).join('')}
2332        </section>`;
2333    }
2334    
2335    // Add table of contents
2336    if (settings.includeToc) {
2337        html += `
2338        <div class="toc">
2339            <h2>${escapeHtml(getTocLabelForExport())}</h2>`;
2340            
2341        chapters.forEach((chapter, index) => {
2342            const titleElement = chapter.querySelector('h2');
2343            const chapterTitle = titleElement ? 
2344                titleElement.textContent.replace(/^\d+\.\s*/, '') : 
2345                `Chapter ${index + 1}`;
2346                
2347            html += `
2348            <div class="toc-entry">
2349                <span class="toc-number">${index + 1}.</span>
2350                <a href="#chapter-${index + 1}" class="toc-title">${escapeHtml(chapterTitle)}</a>
2351                <span class="toc-dots"></span>
2352            </div>`;
2353        });
2354        
2355        html += `
2356        </div>`;
2357    }
2358    
2359    // Add chapters
2360    const allSourcesHTML = []; // For end-of-book placement
2361
2362    chapters.forEach((chapter, index) => {
2363        const titleElement = chapter.querySelector('h2');
2364        const chapterTitle = titleElement ? 
2365            titleElement.textContent : 
2366            `Chapter ${index + 1}`;
2367            
2368        html += `
2369        <div class="chapter" id="chapter-${index + 1}">
2370            <h2>${escapeHtml(chapterTitle)}</h2>`;
2371            
2372        // Get paragraphs
2373        const paragraphs = chapter.querySelectorAll('.chapter-content > p');
2374        paragraphs.forEach(paragraph => {
2375            if (paragraph.textContent.trim()) {
2376                html += `
2377            <p>${escapeHtml(paragraph.textContent)}</p>`;
2378            }
2379        });
2380        
2381        // Add images if enabled
2382        if (settings.includeImages) {
2383            const images = chapter.querySelectorAll('.chapter-content .image-container img');
2384            images.forEach(image => {
2385                try {
2386                    const imageClass = image.classList.contains('round-cutout') ? 'chapter-image round-cutout' : 'chapter-image';
2387                    html += `
2388            <img src="${image.src}" alt="Chapter illustration" class="${imageClass}">`;
2389                } catch (e) {
2390                    console.warn('Error processing image for HTML export:', e);
2391                }
2392            });
2393        }
2394
2395        // Export Sources & Links if present
2396        const sourcesContainer = chapter.querySelector('.chapter-content .chapter-sources-container');
2397        if (sourcesContainer && sourcesContainer.childNodes.length > 0) {
2398            if (settings.sourcesPlacement === 'per-chapter') {
2399                html += `<div class="chapter-sources-container">`;
2400                sourcesContainer.childNodes.forEach(child => {
2401                    if (child.nodeType === Node.ELEMENT_NODE) { // Ensure it's an element
2402                        html += child.outerHTML;
2403                    }
2404                });
2405                html += `</div>`;
2406            } else if (settings.sourcesPlacement === 'end-of-book') {
2407                sourcesContainer.querySelectorAll('.chapter-source-link').forEach(linkDiv => {
2408                     allSourcesHTML.push(`<div class="chapter-source-link">${linkDiv.innerHTML}</div>`);
2409                });
2410            }
2411        }
2412        
2413        html += `
2414        </div>`;
2415    });
2416
2417    // Add combined sources at the end if selected
2418    if (settings.sourcesPlacement === 'end-of-book' && allSourcesHTML.length > 0) {
2419        html += `
2420        <div class="book-sources-end">
2421            <h2>Sources & References</h2>
2422            ${allSourcesHTML.join('')}
2423        </div>`;
2424    }
2425
2426    html += `
2427    </div>
2428</body>
2429</html>`;
2430    
2431    // Create download link
2432    const blob = new Blob([html], { type: 'text/html' });
2433    const url = URL.createObjectURL(blob);
2434    const a = document.createElement('a');
2435    a.href = url;
2436    const filename = `${bookTitle.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.html`;
2437    a.download = filename;
2438    document.body.appendChild(a);
2439    a.click();
2440    document.body.removeChild(a);
2441    URL.revokeObjectURL(url);
2442}
2443
2444/**
2445 * Exports the book to RTF format.
2446 * @param {string} bookTitle - The title of the book
2447 * @param {Array<HTMLElement>} chapters - Array of chapter elements
2448 * @param {Object} settings - Export settings
2449 */
2450// Converts a #rrggbb hex color into RTF \red\green\blue control words.
2451function rtfColor(hex) {
2452    const h = String(hex || '#262626').replace('#', '');
2453    const r = parseInt(h.substring(0, 2), 16);
2454    const g = parseInt(h.substring(2, 4), 16);
2455    const b = parseInt(h.substring(4, 6), 16);
2456    return `\\red${r}\\green${g}\\blue${b}`;
2457}
2458
2459// Converts an image URL into a centered RTF \pict paragraph ('' when it fails).
2460async function rtfImageBlock(src) {
2461    const data = await loadImageDataURL(src);
2462    if (!data) return '';
2463    const comma = data.indexOf(',');
2464    if (comma < 0) return '';
2465    const meta = data.slice(0, comma);
2466    const b64 = data.slice(comma + 1);
2467    const isPng = /png/i.test(meta);
2468    let hex = '';
2469    try {
2470        const bin = atob(b64);
2471        for (let i = 0; i < bin.length; i++) hex += ('0' + bin.charCodeAt(i).toString(16)).slice(-2);
2472    } catch (_) { return ''; }
2473    const kind = isPng ? '\\pngblip' : '\\jpegblip';
2474    return `\\par\\pard\\qc${kind}\\picscalex100\\picscaley100 ${hex}\\par`;
2475}
2476
2477async function exportToRTF(bookTitle, chapters, settings, statusMessage) {
2478    statusMessage.textContent = 'Writing clean RTF document...';
2479    const tmpl = getTemplateDef((settings && settings.template));
2480    const C = tmpl.colors;
2481    const ed = (settings && settings.editorial) || getEditorialSettings();
2482    let authorText = (ed.author || '').trim();
2483    if (!authorText) authorText = await getExportAuthor();
2484
2485    const serif = tmpl.serif || 'Times New Roman';
2486    const sans = tmpl.sans || 'Arial';
2487    const mono = 'Courier New';
2488    const bodyHalf = Math.round((tmpl.bodySize || 11) * 2);   // \fs value
2489    const smallHalf = 16;
2490    const titleHalf = 52;
2491    const subHalf = 30;
2492    const chapHalf = Math.min(52, bodyHalf + 26);
2493    const frontHalf = 32;
2494    const leading = Math.round((tmpl.bodySize || 11) * (tmpl.lineHeight || 1.5) * 20 * 2);
2495
2496    // Color palette table (index -> use).
2497    const COLORS = [C.text, C.heading, C.accent, C.muted, C.rule, C.coverBg, C.coverText];
2498    const colorTbl = '{\\colortbl;' +
2499        COLORS.map(rtfColor).join(';') + ';}';
2500
2501    const header = `{\\rtf1\\ansi\\ansicpg1252\\deff0
2502{\\fonttbl{\\f0\\froman\\fcharset0 ${serif};}{\\f1\\fswiss\\fcharset0 ${sans};}{\\f2\\fmodern\\fcharset0 ${mono};}}
2503${colorTbl}
2504{\\info{\\title ${escapeRtf(bookTitle)}}}
2505\\viewkind4\\uc1\\pgbrdropt\\widctlpar\\f0\\fs${bodyHalf}`;
2506
2507    // Repeating page furniture — running header + centred page-number footer.
2508    let rtf = header +
2509        `\\header\\pard\\qr\\fs18\\cf4 ${escapeRtf(bookTitle)}\\par` +
2510        `\\footer\\pard\\qc\\fs18\\cf4\\chpgn\\par`;
2511
2512    // ================= COVER (title page) =================
2513    if (settings.includeCover) {
2514        rtf += `\\pard\\qc\\sa400\\cf2\\b\\fs${titleHalf} ${escapeRtf(bookTitle)}\\b0\\fs${bodyHalf}\\par`;
2515        if (tmpl.ornament) rtf += `\\pard\\qc\\sa200\\cf5\\cb1\\'\\par`;
2516        if (ed.showSubtitle && ed.subtitle) rtf += `\\pard\\qc\\sa200\\cf3\\i\\fs${subHalf} ${escapeRtf(ed.subtitle)}\\i0\\fs${bodyHalf}\\par`;
2517        else if (ed.subtitle) rtf += `\\pard\\qc\\sa200\\cf3\\i\\fs${subHalf} ${escapeRtf(ed.subtitle)}\\i0\\fs${bodyHalf}\\par`;
2518        if (tmpl.ornament) rtf += `\\pard\\qc\\sa400\\cf5\\cb1\\'\\par`;
2519        if (ed.showAuthor && authorText) rtf += `\\pard\\qc\\sa200\\fs22 ${escapeRtf(authorText)}\\fs${bodyHalf}\\par`;
2520        if (ed.showImprint && ed.imprint) rtf += `\\pard\\qc\\sa200\\cf4\\fs20 ${escapeRtf(ed.imprint)}\\fs${bodyHalf}\\par`;
2521        if (ed.showEdition && ed.edition) rtf += `\\pard\\qc\\sa200\\cf4\\fs20 ${escapeRtf(ed.edition)}\\fs${bodyHalf}\\par`;
2522        rtf += `\\pard\\qc\\sa200\\cf4\\fs18 ${escapeRtf(String(ed.copyrightYear || new Date().getFullYear()))}\\fs${bodyHalf}\\par\\page`;
2523    }
2524
2525    // ================= HALF TITLE (optional, when not using full cover) =================
2526    if (ed.showHalfTitle !== false && !settings.includeCover) {
2527        rtf += `\\pard\\qc\\sa6000\\cf2\\b\\fs40 ${escapeRtf(bookTitle)}\\b0\\fs${bodyHalf}\\par\\page`;
2528    }
2529
2530    // ================= DEDICATION =================
2531    if (ed.showDedication && ed.dedication) {
2532        rtf += `\\pard\\qc\\sl${leading}\\slmult1\\sa3000\\cf3\\i\\fs24 ${escapeRtf(ed.dedication)}\\i0\\fs${bodyHalf}\\par\\page`;
2533    }
2534
2535    // ================= EPIGRAPH =================
2536    if (ed.showEpigraph && ed.epigraph) {
2537        rtf += `\\pard\\qc\\sl${leading}\\slmult1\\sa2400\\cf3\\i\\fs24 ${escapeRtf(ed.epigraph)}\\i0\\fs${bodyHalf}\\par`;
2538        if (ed.epigraphSource) rtf += `\\pard\\qr\\sa200\\cf4\\fs20 \\'97 ${escapeRtf(ed.epigraphSource)}\\fs${bodyHalf}\\par`;
2539        rtf += `\\page`;
2540    }
2541
2542    // ================= FOREWORD =================
2543    if (ed.showForeword && ed.foreword) {
2544        rtf += `\\pard\\qc\\sa200\\cf2\\b\\fs${frontHalf} Foreword\\b0\\fs${bodyHalf}\\par`;
2545        ed.foreword.split(/\n{2,}/).filter(Boolean).forEach(par => {
2546            rtf += `\\pard\\qr${tmpl.align === 'justify' ? '\\qj' : '\\ql'}\\sa200\\sl${leading}\\slmult1\\fs${bodyHalf} ${escapeRtf(par.trim())}\\par`;
2547        });
2548        rtf += `\\page`;
2549    }
2550
2551    // ================= ACKNOWLEDGMENTS =================
2552    if (ed.showAcknowledgments && ed.acknowledgments) {
2553        rtf += `\\pard\\qc\\sa200\\cf2\\b\\fs${frontHalf} Acknowledgments\\b0\\fs${bodyHalf}\\par`;
2554        ed.acknowledgments.split(/\n{2,}/).filter(Boolean).forEach(par => {
2555            rtf += `\\pard\\qj\\sa200\\sl${leading}\\slmult1\\fs${bodyHalf} ${escapeRtf(par.trim())}\\par`;
2556        });
2557        rtf += `\\page`;
2558    }
2559
2560    // ================= EDITORIAL (in front of the TOC) =================
2561    if (ed.includeEditorial && ed.editorialText) {
2562        rtf += `\\pard\\qc\\sa200\\cf2\\b\\fs${frontHalf} Editorial\\b0\\fs${bodyHalf}\\par`;
2563        ed.editorialText.split(/\n{2,}/).filter(Boolean).forEach(par => {
2564            rtf += `\\pard\\qj\\sa200\\sl${leading}\\slmult1\\fs${bodyHalf} ${escapeRtf(par.trim())}\\par`;
2565        });
2566        rtf += `\\page`;
2567    }
2568
2569    // ================= COPYRIGHT (ONLY when explicitly enabled) =================
2570    if (settings.includeCopyright) {
2571        const holder = (ed.copyrightHolder || authorText || 'The Author').trim();
2572        const year = ed.copyrightYear || new Date().getFullYear();
2573        const rights = ed.rightsText || 'All rights reserved. No part of this publication may be reproduced, distributed, or transmitted in any form without the prior written permission of the publisher.';
2574        rtf += `\\pard\\qc\\sa2000\\cf2\\b\\fs${bodyHalf} ${escapeRtf(bookTitle)}\\b0\\fs${bodyHalf}\\par`;
2575        rtf += `\\pard\\qc\\sa200\\cf4\\fs18 Copyright \\u169? ${year} ${escapeRtf(holder)}\\fs${bodyHalf}\\par`;
2576        rtf += `\\pard\\qc\\sa200\\cf4\\fs18 ${escapeRtf(rights)}\\fs${bodyHalf}\\par`;
2577        if (ed.showEdition && ed.edition) rtf += `\\pard\\qc\\sa200\\cf4\\fs18 ${escapeRtf(ed.edition)}\\fs${bodyHalf}\\par`;
2578        if (ed.showIsbn && ed.isbn) rtf += `\\pard\\qc\\sa200\\cf4\\fs18 ISBN ${escapeRtf(ed.isbn)}
2578\\fs${bodyHalf}\\par`;
2579        if (ed.showCredits && ed.credits) rtf += `\\pard\\qc\\sa200\\cf4\\fs18 ${escapeRtf(ed.credits)}\\fs${bodyHalf}\\par`;
2580        rtf += `\\page`;
2581    }
2582
2583    // ================= PREFACE =================
2584    let fm = { preface: '', aboutAuthor: '' };
2585    if (settings.includePreface || settings.includeAboutAuthor) {
2586        try { fm = await generateFrontMatter(bookTitle, chapters); } catch (_) {}
2587    }
2588    if (settings.includePreface && fm.preface) {
2589        rtf += `\\pard\\qc\\sa200\\cf2\\b\\fs${frontHalf} ${escapeRtf(getPrefaceLabel())}\\b0\\fs${bodyHalf}\\par`;
2590        fm.preface.split(/\n{2,}/).filter(Boolean).forEach(par => {
2591            rtf += `\\pard\\qj\\sa200\\sl${leading}\\slmult1\\fs${bodyHalf} ${escapeRtf(par.trim())}\\par`;
2592        });
2593        rtf += `\\page`;
2594    }
2595
2596    // ================= TABLE OF CONTENTS =================
2597    if (settings.includeToc) {
2598        rtf += `\\pard\\qc\\sa200\\cf2\\b\\fs${frontHalf} ${escapeRtf(getTocLabelForExport())}\\b0\\fs${bodyHalf}\\par`;
2599        chapters.forEach((chapter, index) => {
2600            const titleElement = chapter.querySelector('h2');
2601            const chapterTitle = titleElement ?
2602                titleElement.textContent.replace(/^\d+[.:) ]*\s*/, '') :
2603                `Chapter ${index + 1}`;
2604            // Right-aligned dot-leader tab + a PAGE REF field (Word fills it in).
2605            rtf += `{\\pard\\tqr\\tldot\\tx9940\\sa140\\fs${bodyHalf} Chapter ${index + 1}: ${escapeRtf(chapterTitle)}`;
2606            rtf += `\\tab{\\field{\\*\\fldinst{PAGEREF _Ch${index} \\\\h}}{\\fldrslt }}`;
2607            rtf += `\\par}`;
2608        });
2609        rtf += `\\page`;
2610    }
2611
2612    // ================= CHAPTERS =================
2613    const allSourcesRTF = [];
2614    for (let index = 0; index < chapters.length; index++) {
2615        const chapter = chapters[index];
2616        const titleElement = chapter.querySelector('h2');
2617        let chapterTitle = titleElement ? titleElement.textContent : `Chapter ${index + 1}`;
2618        if (tmpl.uppercaseChapters) chapterTitle = String(chapterTitle).toUpperCase();
2619
2620        // Chapter heading (bookmarked so the TOC page-ref can target it).
2621        rtf += `{\\bkmkstart _Ch${index}}`;
2622        rtf += `\\pard\\qc\\sa200\\cf2\\b\\fs${chapHalf} ${escapeRtf(chapterTitle)}\\b0\\fs${bodyHalf}\\par`;
2623        rtf += `{\\bkmkend _Ch${index}}`;
2624        if (tmpl.ornament) rtf += `\\pard\\qc\\sa200\\cf5\\cb1\\'\\par`;
2625
2626        // Images inside the chapter.
2627        if (settings.includeImages !== false) {
2628            const imgs = Array.from(chapter.querySelectorAll('.chapter-content .image-container img'));
2629            for (const img of imgs) {
2630                const src = (img.currentSrc || img.src || '');
2631                rtf += await rtfImageBlock(src);
2632            }
2633        }
2634
2635        // Body paragraphs, with a decorative enlarged initial on the first.
2636        const paragraphs = chapter.querySelectorAll('.chapter-content > p');
2637        let firstPara = true;
2638        for (const paragraph of paragraphs) {
2639            const text = (paragraph.textContent || '').replace(/\s+/g, ' ').trim();
2640            if (!text) continue;
2641            if (firstPara && tmpl.dropcaps) {
2642                firstPara = false;
2643                const ch = Array.from(text)[0];
2644                const rest = text.slice(ch.length);
2645                rtf += `\\pard\\sa200\\cf3\\b\\fs${bodyHalf + 28} ${escapeRtf(ch)}\\b0\\cf1\\fs${bodyHalf} ${escapeRtf(rest)}\\par`;
2646            } else {
2647                rtf += `\\pard\\qj\\sa200\\sl${leading}\\slmult1\\fs${bodyHalf} ${escapeRtf(text)}\\par`;
2648            }
2649        }
2650
2651        // Sources & links.
2652        const sourcesContainer = chapter.querySelector('.chapter-content .chapter-sources-container');
2653        if (sourcesContainer && sourcesContainer.querySelectorAll('.chapter-source-link').length) {
2654            let sourcesContentForChapter = '';
2655            if (settings.sourcesPlacement === 'per-chapter') {
2656                sourcesContentForChapter += `\\pard\\sa200\\cf2\\b\\fs24 Sources & Links:\\b0\\cf1\\fs${bodyHalf}\\par`;
2657            }
2658            sourcesContainer.querySelectorAll('.chapter-source-link').forEach(linkDiv => {
2659                const aTag = linkDiv.querySelector('a');
2660                const title = aTag ? aTag.textContent : 'Untitled Source';
2661                const url = aTag ? aTag.href : '';
2662                const typeSpan = linkDiv.querySelector('span:not([style])') || linkDiv.querySelector('span');
2663                const type = typeSpan ? typeSpan.textContent : '';
2664                const descSpan = linkDiv.querySelectorAll('span')[1];
2665                const description = descSpan ? descSpan.textContent : '';
2666
2667                let sourceRtf = `{\\pard\\li720 ${escapeRtf(title)}`;
2668                if (type) sourceRtf += ` (${escapeRtf(type)})`;
2669                if (url) sourceRtf += ` {\\field{\\*\\fldinst{HYPERLINK "${escapeRtf(url)}"}}{\\fldrslt{\\ul\\cf3 ${escapeRtf(url)}}}}`;
2670                sourceRtf += `\\par}`;
2671                if (description) {
2672                    sourceRtf += `{\\pard\\li900\\sa120\\cf4\\fs18 ${escapeRtf(description)}\\par}`;
2673                }
2674                if (settings.sourcesPlacement === 'per-chapter') sourcesContentForChapter += sourceRtf;
2675                else if (settings.sourcesPlacement === 'end-of-book') allSourcesRTF.push(sourceRtf);
2676            });
2677
2678            if (settings.sourcesPlacement === 'per-chapter' && sourcesContentForChapter) {
2679                rtf += sourcesContentForChapter;
2680            }
2681        }
2682
2683        if (index < chapters.length - 1) rtf += `\\page`;
2684    }
2685
2686    // ================= END-OF-BOOK SOURCES =================
2687    if (settings.sourcesPlacement === 'end-of-book' && allSourcesRTF.length) {
2688        rtf += `\\page\\pard\\qc\\sa200\\cf2\\b\\fs${frontHalf}
2688 Sources & References\\b0\\fs${bodyHalf}\\par`;
2689        allSourcesRTF.forEach(sourceRtf => { rtf += sourceRtf; });
2690    }
2691
2692    // ================= ABOUT THE AUTHOR (back matter, when enabled) =================
2693    if (settings.includeAboutAuthor && fm.aboutAuthor) {
2694        rtf += `\\page\\pard\\qc\\sa200\\cf2\\b\\fs${frontHalf} ${escapeRtf(getAboutAuthorLabel())}\\b0\\fs${bodyHalf}\\par`;
2695        fm.aboutAuthor.split(/\n{2,}/).filter(Boolean).forEach(par => {
2696            rtf += `\\pard\\qj\\sa200\\sl${leading}\\slmult1\\fs${bodyHalf} ${escapeRtf(par.trim())}\\par`;
2697        });
2698    }
2699
2700    rtf += `}`;
2701
2702    const blob = new Blob([rtf], { type: 'application/rtf' });
2703    const url = URL.createObjectURL(blob);
2704    const a = document.createElement('a');
2705    a.href = url;
2706    const filename = `${bookTitle.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.rtf`;
2707    a.download = filename;
2708    document.body.appendChild(a);
2709    a.click();
2710    document.body.removeChild(a);
2711    URL.revokeObjectURL(url);
2712}
2713function escapeXml(text) {
2714    return String(text == null ? '' : text)
2715        .replace(/&/g, '&amp;')
2716        .replace(/</g, '&lt;')
2717        .replace(/>/g, '&gt;')
2718        .replace(/"/g, '&quot;')
2719        .replace(/'/g, '&apos;');
2720}
2721
2722/**
2723 * Loads JSZip from CDN for building OOXML packages.
2724 */
2725async function importJSZip() {
2726    if (window.JSZip) return window.JSZip;
2727    if (!document.getElementById('jszip-cdn')) {
2728        const script = document.createElement('script');
2729        script.id = 'jszip-cdn';
2730        script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js';
2731        document.head.appendChild(script);
2732        await new Promise((resolve, reject) => { script.onload = resolve; script.onerror = reject; });
2733    }
2734    return window.JSZip;
2735}
2736
2737/**
2738 * Uses the AI to write professional front/back matter (preface and about-the-author)
2739 * matched to the book. Falls back to neutral, well-written templates if the AI is
2740 * unavailable, so exports never block or fail.
2741 * @returns {Promise<{preface:string, aboutAuthor:string, subtitle:string}>}
2742 */
2743async function generateFrontMatter(bookTitle, chapters) {
2744    const sample = chapters.slice(0, 3).map(ch =>
2745        ch.querySelector('h2')?.textContent || 'Chapter'
2746    ).join(', ');
2747    const lazy = {
2748        preface: `This book is the result of a deep and deliberate inquiry into its subject. Within these pages, you will find a clear, well-ordered treatment of the ideas at the heart of the matter, presented so that each chapter builds naturally on the last. I have written it to reward both the newcomer seeking an accessible entry point and the seasoned reader looking for a fresh, rigorous perspective.\n\nMy hope is that ${bookTitle} proves as useful and illuminating to you as the process of writing it has been to me. Whatever your background, may you find the time you spend here well spent.`,
2749        aboutAuthor: `The author of ${bookTitle} is a writer devoted to making complex ideas clear, compelling and human. With a deep interest in the book's field and a commitment to thoughtful, well-researched storytelling, they approach every subject with curiosity, precision and warmth. This book reflects that care — written to inform, to engage, and to stay with you long after the final page.`
2750    };
2751    try {
2752        const prompt = `You are the author of the finished book titled "${bookTitle}". Its chapters are: ${sample}. Using the writing voice appropriate to the book, write two short front/back-matter pieces in the book's language:
2753
27541. "preface": 80-120 words welcoming the reader and setting expectations. Written in first person, warm and professional, WITHOUT any placeholder brackets.
27552. "aboutAuthor": 50-80 words describing the author in a believable, modest, professional way. Use a generic but convincing author profile; do NOT use a real person's name.
27563. "subtitle": a single elegant subtitle sentence (10-18 words) for the book.
2757
2758Respond in JSON only: {"preface": string, "aboutAuthor": string, "subtitle": string}`;
2759        const completion = await chatComplete(
2760            [{ role: 'system', content: prompt }, { role: 'user', content: bookTitle }
2760],
2761            { json: true, temperature: 0.6 }
2762        );
2763        const parsed = safeJsonParse(completion.content);
2764        return {
2765            preface: parsed.preface || lazy.preface,
2766            aboutAuthor: parsed.aboutAuthor || lazy.aboutAuthor,
2767            subtitle: parsed.subtitle || ''
2768        };
2769    } catch (e) {
2770        console.warn('Front matter generation fell back to templates.', e);
2771        return lazy;
2772    }
2773}
2774
2775/**
2776 * Exports the book as a fully editable Word (.docx) document using real Word styles
2777 * (Heading 1.., Normal, TOC field) and section/page breaks.
2778 */
2779// Returns the pixel dimensions of an image encoded as a data URL.
2780function docxImageDims(dataUrl) {
2781    return new Promise((res) => {
2782        const i = new Image();
2783        i.onload = () => res({ w: i.naturalWidth, h: i.naturalHeight });
2784        i.onerror = () => res({ w: 0, h: 0 });
2785        i.src = dataUrl;
2786    });
2787}
2788
2789// Inline-wordprocessingDrawing for an image, sized in EMU and centred later.
2790function docxInlineImage(rId, wEMU, hEMU, docId) {
2791    const A = 'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"';
2792    const PIC = 'xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"';
2793    return `<w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${wEMU}" cy="${hEMU}"/><wp:effectExtent l="0" t="0" r="0" b="0"/><wp:docPr id="${docId}" name="Image ${docId}"/><wp:cNvGraphicFramePr><a:graphicFrameLocks ${A} noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic ${A}><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic ${PIC}><pic:nvPicPr><pic:cNvPr id="${docId}" name="Image ${docId}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${rId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${wEMU}" cy="${hEMU}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing>`;
2794}
2795
2796// img = { dataUrl } ; returns a centred paragraph with the scaled image.
2797function docxImageParagraph(img, docId) {
2798    const MAX_W = 5730000; // ~58% of an A4 line at these margins (EMU)
2799    let w = img.wEMU || 0;
2800    let h = img.hEMU || 0;
2801    if (w > MAX_W) { h = Math.round(h * (MAX_W / w)); w = MAX_W; }
2802    if (!w) return `<w:p/>`;
2803    return `<w:p><w:pPr><w:jc w:val="center"/><w:spacing w:before="200" w:after="200"/></w:pPr><w:r>${docxInlineImage(img.rId, w, h, docId)}</w:r></w:p>`;
2804}
2805
2806/**
2807 * Exports the book as a fully editable Word (.docx) document using real Word
2808 * styles, an auto-updating table of contents, images, footers with page
2809 * numbers, and every editorial / front-matter section.
2810 */
2811async function exportToDOCX(bookTitle, chapters, settings, statusMessage) {
2812    const JSZip = await importJSZip();
2813    statusMessage.textContent = 'Generating Word document...';
2814    const tmpl = getTemplateDef((settings && settings.template));
2815    const C = tmpl.colors;
2816    const ed = (settings && settings.editorial) || getEditorialSettings();
2817    let authorText = (ed.author || '').trim();
2818    if (!authorText) authorText = await getExportAuthor();
2819    let fm = { preface: '', aboutAuthor: '' };
2820    if (settings.includePreface || settings.includeAboutAuthor) {
2821        try { fm = await generateFrontMatter(bookTitle, chapters); } catch (_) {}
2822    }
2823    const now = new Date();
2824
2825    const bodyHalf = Math.round((tmpl.bodySize || 11) * 2);          // half-points
2826    const smallHalf = 16;
2827    const headingHalf = Math.min(56, bodyHalf + 24);
2828    const titleHalf = 64;
2829    const subHalf = Math.round(bodyHalf + 8);
2830    const accentHex = (C.accent || '5c6bc0').replace('#', '');
2831    const headingHex = (C.heading || '1f2430').replace('#', '');
2832    const bodyHex = (C.text || '262626').replace('#', '');
2833    const mutedHex = (C.muted || '767676').replace('#', '');
2834
2835    const escape = escapeXml;
2836    const rPr = (opts) => {
2837        const parts = [];
2838        const size = opts.size != null ? `<w:sz w:val="${opts.size}"/><w:szCs w:val="${opts.size}"/>` : '';
2839        const font = opts.font ? `<w:rFonts w:ascii="${opts.font}" w:hAnsi="${opts.font}" w:eastAsia="${opts.font}"/>` : '';
2840        const color = opts.color ? `<w:color w:val="${opts.color}"/>` : '';
2841        if (opts.bold) parts.push('<w:b/><w:bCs/>');
2842        if (opts.italic) parts.push('<w:i/><w:iCs/>');
2843        if (opts.underline) parts.push('<w:u w:val="single"/>');
2844        return `<w:rPr>${font}${parts.join('')}${color}${size}</w:rPr>`;
2845    };
2846    const run = (text, opts) => `<w:r>${rPr(opts)}<w:t xml:space="preserve">${escape(text)}</w:t></w:r>`;
2847    const para = (pPr, ...runs) => `<w:p><w:pPr>${pPr || ''}</w:pPr>${runs.join('')}</w:p>`;
2848    const styleP = (style, extra) => `<w:pStyle w:val="${style}"/>${extra || ''}`;
2849    const breakPage = () => para('', `<w:r><w:br w:type="page"/></w:r>`);
2850    const fmt = '<w:r><w:fldChar w:fldCharType="begin"/><w:instrText xml:space="preserve"> PAGE </w:instrText><w:fldChar w:fldCharType="separate"/><w:t>1</w:t><w:fldChar w:fldCharType="end"/></w:r>';
2851
2852    // --- Preload + size chapter images in document order ---
2853    const imagePlan = [];
2854    chapters.forEach((ch) => {
2855        ch.querySelectorAll('.chapter-content .image-container img').forEach((img) => {
2856            const src = (img.currentSrc || img.src || '');
2857            imagePlan.push(src.startsWith('data:') ? src : null);
2858        });
2859    });
2860    const imageMeta = [];
2861    const PRELOAD = async (plan) => {
2862        for (const src of plan) {
2863            if (!src) { imageMeta.push(null); continue; }
2864            const dataUrl = await loadImageDataURL(src);
2865            if (!dataUrl) { imageMeta.push(null); continue; }
2866            const dims = await docxImageDims(dataUrl);
2867            imageMeta.push({ dataUrl, dm: dims });
2868        }
2869    };
2870    await PRELOAD(imagePlan);
2871
2872    const body = [];
2873    const imageRels = [];
2874    const extRels = [];
2875    let imgCounter = 0;
2876    const chapterImageRefs = chapters.map(() => []);
2877    chapters.forEach((ch, ci) => {
2878        let pi = 0;
2879        ch.querySelectorAll('.chapter-content .image-container img').forEach(() => {
2880            const meta = imageMeta[pi++];
2881            if (!meta) return;
2882            imgCounter++;
2883            const ext = /png/i.test(meta.dataUrl.slice(0, 100)) ? 'png' : 'jpg';
2884            const rid = `rIdImg${imgCounter}`;
2885            imageRels.push({ rid, dataUrl: meta.dataUrl, ext });
2886            // convert pixels -> EMU (914400 / 96dpi)
2887            const emuW = Math.round((meta.dm.w || 640) * 9525);
2888            const emuH = Math.round((meta.dm.h || 480) * 9525);
2889            chapterImageRefs[ci].push({ rid, wEMU: emuW, hEMU: emuH });
2890        });
2891    });
2892
2893    // ------------------------------------------------------------------ body
2894    // Half-title page (optional)
2895    if (ed.showHalfTitle !== false) {
2896        body.push(para(styleP('NoSpacing','<w:jc w:val="center"/><w:spacing w:before="4400"/>'), run(bookTitle, { size: titleHalf, bold: true, color: headingHex, font: tmpl.serif })));
2897        body.push(breakPage());
2898    }
2899
2900    // Full title / cover page
2901    if (settings.includeCover) {
2902        body.push(para(styleP('NoSpacing','<w:spacing w:before="3600"/>')));
2903        if (tmpl.ornament) body.push(para(styleP('NoSpacing','<w:jc w:val="center"/><w:spacing w:after="240"/>'), `<w:r><w:pict><v:shape xmlns:v="urn:schemas-microsoft-com:vml" style="width:140pt;height:0.75pt"><v:stroke color="${accentHex}" weight="0.75pt"/><v:fill/></v:shape></w:pict></w:r>`));
2904        body.push(para(styleP('Title','<w:jc w:val="center"/>'), run(bookTitle, { size: titleHalf, bold: true, color: headingHex })));
2905        const sub = (ed.showSubtitle && ed.subtitle) || ed.subtitle;
2906        if (sub) body.push(para(styleP('Subtitle','<w:jc w:val="center"/>'), run(sub, { size: subHalf, italic: true, color: mutedHex })));
2907        if (tmpl.ornament) body.push(para(styleP('NoSpacing','<w:jc w:val="center"/><w:spacing w:before="0"/>'), `<w:r><w:pict><v:shape xmlns:v="urn:schemas-microsoft-com:vml" style="width:80pt;height:0.5pt"><v:stroke color="${accentHex}" weight="0.5pt"/></v:shape></w:pict></w:r>`));
2908        body.push(para(styleP('NoSpacing','<w:spacing w:before="1200"/>')));
2909        if (ed.showAuthor && authorText) body.push(para(styleP('Title','<w:jc w:val="center"/>'), run(authorText, { size: subHalf, color: bodyHex })));
2910        if (ed.showImprint && ed.imprint) body.push(para(styleP('Subtitle','<w:jc w:val="center"/>'), run(ed.imprint, { size: smallHalf, color: mutedHex })));
2911        if (ed.showEdition && ed.edition) body.push(para(styleP('Subtitle','<w:jc w:val="center"/>'), run(ed.edition, { size: smallHalf, color: mutedHex })));
2912        body.push(breakPage());
2913    }
2914
2915    // Dedication (optional)
2916    if (ed.showDedication && ed.dedication) {
2917        body.push(para(styleP('NoSpacing','<w:jc w:val="center"/><w:spacing w:before="3600"/>'), run(ed.dedication, { size: subHalf, italic: true, color: bodyHex })));
2918        body.push(breakPage());
2919    }
2920
2921    // Epigraph (optional)
2922    if (ed.showEpigraph && ed.epigraph) {
2923        body.push(para(styleP('NoSpacing','<w:jc w:val="center"/><w:spacing w:before="2800"/>'), run(ed.epigraph, { size: subHalf, italic: true, color: bodyHex })));
2924        if (ed.epigraphSource) body.push(para(styleP('Normal','<w:jc w:val="r
2924ight"/>'), run('\u2014 ' + ed.epigraphSource, { size: smallHalf, color: mutedHex })));
2925        body.push(breakPage());
2926    }
2927
2928    // Foreword (optional)
2929    if (ed.showForeword && ed.foreword) {
2930        body.push(para(styleP('Heading1'), run('Foreword', { bold: true, color: headingHex, font: tmpl.serif })));
2931        ed.foreword.split(/\n{2,}/).filter(Boolean).forEach((p) => body.push(para(styleP('Normal','<w:jc w:val="both"/>'), run(p.trim(), { size: bodyHalf, color: bodyHex }))));
2932        body.push(breakPage());
2933    }
2934
2935    // Acknowledgments (optional)
2936    if (ed.showAcknowledgments && ed.acknowledgments) {
2937        body.push(para(styleP('Heading1'), run('Acknowledgments', { bold: true, color: headingHex, font: tmpl.serif })));
2938        ed.acknowledgments.split(/\n{2,}/).filter(Boolean).forEach((p) => body.push(para(styleP('Normal','<w:jc w:val="both"/>'), run(p.trim(), { size: bodyHalf, color: bodyHex }))));
2939        body.push(breakPage());
2940    }
2941
2942    // Editorial / introduction (in front of the TOC)
2943    if (ed.includeEditorial && ed.editorialText) {
2944        body.push(para(styleP('Heading1'), run('Editorial', { bold: true, color: headingHex, font: tmpl.serif })));
2945        ed.editorialText.split(/\n{2,}/).filter(Boolean).forEach((p) => body.push(para(styleP('Normal','<w:jc w:val="both"/>'), run(p.trim(), { size: bodyHalf, color: bodyHex }))));
2946        body.push(breakPage());
2947    }
2948
2949    // Copyright / legal page (ONLY when explicitly enabled)
2950    if (settings.includeCopyright) {
2951        const holder = (ed.copyrightHolder || authorText || 'The Author').trim();
2952        const year = ed.copyrightYear || now.getFullYear();
2953        const rights = ed.rightsText || 'All rights reserved. No part of this publication may be reproduced, distributed, or transmitted in any form without the prior written permission of the publisher.';
2954        body.push(para(styleP('NoSpacing','<w:spacing w:before="2000"/>')));
2955        body.push(para(styleP('Normal','<w:jc w:val="center"/><w:spacing w:after="200"/>'), run(bookTitle, { bold: true, size: bodyHalf, color: bodyHex })));
2956        body.push(para(styleP('Normal','<w:jc w:val="center"/><w:spacing w:after="200"/>'), run(`Copyright \u00A9 ${year} ${holder}`, { size: smallHalf, color: mutedHex })));
2957        body.push(para(styleP('Normal','<w:jc w:val="center"/><w:spacing w:after="200"/>'), run(rights, { size: smallHalf, color: mutedHex })));
2958        if (ed.showEdition && ed.edition) body.push(para(styleP('Normal','<w:jc w:val="center"/><w:spacing w:after="120"/>'), run(ed.edition, { size: smallHalf, color: mutedHex })));
2959        if (ed.showIsbn && ed.isbn) body.push(para(styleP('Normal','<w:jc w:val="center"/><w:spacing w:after="120"/>'), run(`ISBN ${ed.isbn}`, { size: smallHalf, color: mutedHex })));
2960        if (ed.showCredits && ed.credits) body.push(para(styleP('Normal','<w:jc w:val="center"/>'), run(ed.credits, { size: smallHalf, color: mutedHex })));
2961        body.push(breakPage());
2962    }
2963
2964    // Table of contents (static entries with live PAGEREF page numbers)
2965    if (settings.includeToc) {
2966        body.push(para(styleP('TOCHeading'), run(getTocLabelForExport(), { bold: true, size: headingHalf, color: headingHex, font: tmpl.serif })));
2967        chapters.forEach((c, i) => {
2968            const t = c.querySelector('h2');
2969            const title = t ? t.textContent.replace(/^\d+[.:) ]*\s*/, '') : `Chapter ${i + 1}`;
2970            body.push(para(styleP('TOC1','<w:tabs><w:tab w:val="right" w:leader="dot" w:pos="9026"/></w:tabs>'),
2971                run(`Chapter ${i + 1}: ${title}`, { size: bodyHalf - 2, color: bodyHex }),
2972                '<w:r><w:tab/></w:r>',
2973                `<w:r><w:rPr><w:color w:val="${mutedHex}"/><w:sz w:val="${smallHalf}"/></w:rPr><w:fldChar w:fldCharType="begin"/><w:instrText xml:space="preserve"> PAGEREF _Ch${i} \h </w:instrText><w:fldChar w:fldCharType="separate"/><w:t>0</w:t><w:fldChar w:fldCharType="end"/></w:r>`));
2974        });
2975        body.push(breakPage());
2976    }
2977
2978    // Preface (optional)
2979    if (settings.includePreface && fm.preface) {
2980        body.push(para(styleP('Heading1'), run(getPrefaceLabel(), { bold: true, color: headingHex, font: tmpl.serif })));
2981        fm.preface.split(/\n{2,}
2981/).filter(Boolean).forEach((p) => body.push(para(styleP('Normal','<w:jc w:val="both"/>'), run(p.trim(), { size: bodyHalf, color: bodyHex }))));
2982        body.push(breakPage());
2983    }
2984
2985    // Chapters
2986    const endSources = [];
2987    chapters.forEach((ch, index) => {
2988        const titleEl = ch.querySelector('h2');
2989        let chapterTitle = titleEl ? titleEl.textContent : `Chapter ${index + 1}`;
2990        if (tmpl.uppercaseChapters) chapterTitle = String(chapterTitle).toUpperCase();
2991        const bmStart = `<w:bookmarkStart w:id="${500 + index}" w:name="_Ch${index}"/>`;
2992        const bmEnd = `<w:bookmarkEnd w:id="${500 + index}"/>`;
2993        body.push(`<w:p><w:pPr><w:pStyle w:val="Heading1"/><w:pageBreakBefore/></w:pPr>${bmStart}${run(chapterTitle, { bold: true, color: headingHex, font: tmpl.serif })}${bmEnd}</w:p>`);
2994        if (tmpl.ornament) body.push(para(styleP('NoSpacing','<w:jc w:val="center"/>'), `<w:r><w:pict><v:shape xmlns:v="urn:schemas-microsoft-com:vml" style="width:120pt;height:0.5pt"><v:stroke color="${accentHex}" weight="0.5pt"/></v:shape></w:pict></w:r>`));
2995
2996        // Images
2997        (chapterImageRefs[index] || []).forEach((img) => {
2998            body.push(docxImageParagraph(img, 1000 + index));
2999        });
3000
3001        // Body paragraphs with a drop-cap opening for literary templates.
3002        const paras = ch.querySelectorAll('.chapter-content > p');
3003        let firstPara = true;
3004        for (const p of paras) {
3005            const text = (p.textContent || '').replace(/\s+/g, ' ').trim();
3006            if (!text) continue;
3007            if (firstPara && tmpl.dropcaps && tmpl.serif !== 'Courier New') {
3008                firstPara = false;
3009                const ch0 = Array.from(text)[0];
3010                const rest = text.slice(ch0.length);
3011                body.push(`<w:p><w:pPr><w:pStyle w:val="Normal"/><w:jc w:val="both"/><w:spacing w:after="160" w:line="${Math.round(tmpl.lineHeight * 240)}" w:lineRule="auto"/></w:pPr><w:r><w:rPr><w:sz w:val="${bodyHalf + 30}"/><w:rFonts w:ascii="${tmpl.serif}" w:hAnsi="${tmpl.serif}"/><w:color w:val="${accentHex}"/></w:rPr><w:t xml:space="preserve">${escape(ch0)}</w:t></w:r>${run(rest, { size: bodyHalf, color: bodyHex })}</w:p>`);
3012            } else {
3013                body.push(para(styleP('Normal','<w:jc w:val="both"/><w:ind w:firstLine="360"/><w:spacing w:after="160" w:line="288" w:lineRule="auto"/>'), run(text, { size: bodyHalf, color: bodyHex })));
3014            }
3015        }
3016
3017        // Sources
3018        const srcBox = ch.querySelector('.chapter-content .chapter-sources-container');
3019        if (srcBox && srcBox.querySelectorAll('.chapter-source-link').length) {
3020            if (settings.sourcesPlacement === 'per-chapter') {
3021                body.push(para(styleP('Heading2'), run('Sources & Links', { bold: true, color: headingHex, font: tmpl.serif })));
3022                srcBox.querySelectorAll('.chapter-source-link').forEach((ld) => {
3023                    const aT = ld.querySelector('a');
3024                    const title = aT ? aT.textContent : 'Untitled Source';
3025                    const url = aT ? aT.href : '';
3026                    const spans = ld.querySelectorAll('span');
3027                    const type = spans[0] ? spans[0].textContent : '';
3028                    const desc = spans[1] ? spans[1].textContent : '';
3029                    const label = title + (type ? ` (${type})` : '');
3030                    if (url) {
3031                        const rid = `rIdExt${extRels.length + 1}`;
3032                        extRels.push({ rid, url });
3033                        body.push(`<w:p><w:pPr><w:pStyle w:val="NoSpacing"/><w:ind w:left="360"/></w:pPr><w:hyperlink r:id="${rid}" w:history="1">${run(label, { underline: true, color: accentHex })}</w:hyperlink>${desc ? run(' \u2014 ' + desc, { size: smallHalf, color: mutedHex }) : ''}</w:p>`);
3034                    } else {
3035                        body.push(`<w:p><w:pPr><w:pStyle w:val="NoSpacing"/><w:ind w:left="360"/></w:pPr>${run(label + (desc ? ' \u2014 ' + desc : ''), { size: bodyHalf - 2, color: bodyHex })}</w:p>`);
3036                    }
3037                });
3038            } else if (settings.sourcesPlacement === 'end-of-book') {
3039                const items = [];
3040                srcBox.querySelectorAll('.chapter-source-link').forEach((ld) => {
3041                    const aT = ld.querySelector('a');
3042                    const title = aT ? aT.textContent : 'Untitled Source';
3043                    const url = aT ? aT.href : '';
3044                    const spans = ld.querySelectorAll('span');
3045                    const type = spans[0] ? spans[0].textContent : '';
3046                    const desc = spans[1] ? spans[1].textContent : '';
3047                    items.push({ title, url, type, desc });
3048                });
3049                endSources.push({ chapter: chapterTitle, items });
3050            }
3051        }
3052    });
3053
3054    // End-of-book sources
3055    if (settings.sourcesPlacement === 'end-of-book' && endSources.length) {
3056        body.push(para(styleP('Heading1','<w:pageBreakBefore/>'), run('Sources & References', { bold: true, color: headingHex, font: tmpl.serif })));
3057        endSources.forEach((grp) => {
3058            body.push(para(styleP('Heading2'), run(grp.chapter, { bold: true, color: headingHex })));
3059            grp.items.forEach((s) => {
3060                body.push(`<w:p><w:pPr><w:pStyle w:val="NoSpacing"/><w:ind w:left="360"/></w:pPr>${run(s.title + (s.type ? ` (${s.type})` : ''), { underline: true, color: accentHex })}${s.desc ? run(' \u2014 ' + s.desc, { size: smallHalf, color: mutedHex }) : ''}</w:p>`);
3061            });
3062        });
3063    }
3064
3065    // About the author (back matter, only when enabled)
3066    if (settings.includeAboutAuthor && fm.aboutAuthor) {
3067        body.push(para(styleP('Heading1','<w:pageBreakBefore/>'), run(getAboutAuthorLabel(), { bold: true, color: headingHex, font: tmpl.serif })));
3068        fm.aboutAuthor.split(/\n{2,}/).filter(Boolean).forEach((p) => body.push(para(styleP('Normal','<w:jc w:val="both"/>'), run(p.trim(), { size: bodyHalf, color: bodyHex }))));
3069    }
3070
3071    // ---- document.xml ----
3072    const PAGE_MAP = { A4: ['11906','16838'], LETTER: ['12240','15840'], A5: ['8390','11906'], B5: ['9982','14175'] };
3073    const pg = PAGE_MAP[String(settings.pageSize || 'a4').toUpperCase()] || PAGE_MAP.A4;
3074
3075    const sectPr = `<w:sectPr>
3076      <w:headerReference w:type="default" r:id="rIdH"/>
3077      <w:headerReference w:type="first" r:id="rIdHFirst"/>
3078      <w:footerReference w:type="default" r:id="rIdF"/>
3079      <w:footerReference w:type="first" r:id="rIdFFirst"/>
3080      <w:pgSz w:w="${pg[0]}" w:h="${pg[1]}"/>
3081      <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/>
3082      <w:cols w:space="708"/>
3083      <w:docGrid w:linePitch="360"/>
3084    </w:sectPr>`;
3085
3086    let docXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
3087    docXml += '<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 wp14">';
3088    docXml += '<w:body>';
3089    docXml += body.join('');
3090    docXml += sectPr;
3091    docXml += '</w:body></w:document>';
3092
3093    // ---- header / footer parts ----
3094    const headerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:p><w:pPr><w:pStyle w:val="Header"/>
3094<w:jc w:val="right"/></w:pPr><w:r><w:rPr><w:sz w:val="16"/><w:color w:val="${mutedHex}"/></w:rPr><w:t xml:space="preserve">${escape(bookTitle)}</w:t></w:r></w:p></w:hdr>`;
3095    const emptyHeaderXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:p/></w:hdr>`;
3096    const footerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:p><w:pPr><w:pStyle w:val="Footer"/><w:jc w:val="center"/></w:pPr><w:r><w:rPr><w:sz w:val="18"/><w:color w:val="${mutedHex}"/></w:rPr>${fmt}</w:r></w:p></w:ftr>`;
3097    const emptyFooterXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:p/></w:ftr>`;
3098
3099    // ---- styles.xml ----
3100    const stylesXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3101<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3102  <w:docDefaults>
3103    <w:rPrDefault><w:rPr><w:rFonts w:ascii="${tmpl.serif}" w:hAnsi="${tmpl.serif}" w:eastAsia="${tmpl.serif}"/><w:sz w:val="${bodyHalf}"/><w:szCs w:val="${bodyHalf}"/><w:color w:val="${bodyHex}"/></w:rPr></w:rPrDefault>
3104    <w:pPrDefault><w:pPr><w:spacing w:after="160" w:line="288" w:lineRule="auto"/><w:jc w:val="both"/></w:pPr></w:pPrDefault>
3105  </w:docDefaults>
3106  <w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style>
3107  <w:style w:type="paragraph" w:styleId="NoSpacing"><w:name w:val="No Spacing"/><w:basedOn w:val="Normal"/><w:uiPriority w:val="1"/><w:pPr><w:spacing w:after="0" w:line="240" w:lineRule="auto"/></w:pPr></w:style>
3108  <w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/><w:basedOn w:val="Normal"/><w:qFormat/><w:rPr><w:b/><w:sz w:val="${titleHalf}"/><w:color w:val="${headingHex}"/></w:rPr></w:style>
3109  <w:style w:type="paragraph" w:styleId="Subtitle"><w:name w:val="Subtitle"/><w:basedOn w:val="Normal"/><w:qFormat/><w:rPr><w:i/><w:sz w:val="${subHalf}"/><w:color w:val="${mutedHex}"/></w:rPr></w:style>
3110  <w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:keepLines/><w:spacing w:before="360" w:after="200"/></w:pPr><w:rPr><w:b/><w:sz w:val="${headingHalf}"/><w:color w:val="${headingHex}"/></w:rPr></w:style>
3111  <w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:keepLines/><w:spacing w:before="280" w:after="120"/></w:pPr><w:rPr><w:b/><w:sz w:val="${bodyHalf + 4}"/><w:color w:val="${accentHex}"/></w:rPr></w:style>
3112  <w:style w:type="paragraph" w:styleId="Quote"><w:name w:val="Quote"/><w:basedOn w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:before="240" w:after="240"/><w:ind w:left="720" w:right="720"/></w:pPr><w:rPr><w:i/><w:color w:val="${mutedHex}"/></w:rPr></w:style>
3113  <w:style w:type="paragraph" w:styleId="TOCHeading"><w:name w:val="TOC Heading"/><w:basedOn w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="240" w:after="200"/></w:pPr><w:rPr><w:b/><w:sz w:val="${bodyHalf + 12}"/><w:color w:val="${headingHex}"/></w:rPr></w:style>
3114  <w:style w:type="paragraph" w:styleId="TOC1"><w:name w:val="toc 1"/><w:basedOn w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:after="80" w:line="260" w:lineRule="auto"/></w:pPr></w:style>
3115  <w:style w:type="character" w:styleId="Header"><w:name w:val="header"/><w:basedOn w:val="Normal"/><w:pPr><w:spacing w:after="120"/></w:pPr></w:style>
3116  <w:style w:type="character" w:styleId="Footer"><w:name w:val="footer"/><w:basedOn w:val="Normal"/><w:pPr><w:spacing w:before="120"/></w:pPr></w:style>
3117</w:styles>`;
3118
3119    // ---- relations + content types + media ----
3120    const rIds = imageRels.map((im) => `${im.rid} Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/img${im.rid.slice(5)}.${im.ext}"/>`);
3121    const docRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3122<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3123  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
3124  <Relationship Id="rIdH" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml"/>
3125  <Relationship Id="rIdHFirst" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="headerFirst.xml"/>
3126  <Relationship Id="rIdF" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/>
3127  <Relationship Id="rIdFFirst" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footerFirst.xml"/>
3128  ${rIds.join('\n  ')}
3129  ${extRels.map((l) => `<Relationship Id="${l.rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="${escape(l.url)}" TargetMode="External"/>`).join('\n  ')}
3130</Relationships>`;
3131
3132    const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3133<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
3134  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
3135  <Default Extension="xml" ContentType="application/xml"/>
3136  <Default Extension="png" ContentType="image/png"/>
3137  <Default Extension="jpg" ContentType="image/jpeg"/>
3138  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
3139  <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
3140  <Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"/>
3141  <Override PartName="/word/headerFirst.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"/>
3142  <Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>
3143  <Override PartName="/word/footerFirst.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>
3144  <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
3145  <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
3146</Types>`;
3147
3148    const rootRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3149<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3150  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
3151  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
3152  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
3153</Relationships>`;
3154
3155    const coreProps = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3156<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3157  <dc:title>${escape(bookTitle)}</dc:title>
3158  <dc:creator>${escape(authorText)}</dc:creator>
3159  <cp:lastModifiedBy>AI Book Generator</cp:lastModifiedBy>
3160  <cp:revision>1</cp:revision>
3161  <dcterms:created xsi:type="dcterms:W3CDTF">${now.toISOString()}</dcterms:created>
3162  <dcterms:modified xsi:type="dcterms:W3CDTF">${now.toISOString()}</dcterms:modified>
3163</cp:coreProperties>`;
3164
3165    const appProps = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3166<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
3167  <Application>AI Book Generator</Application>
3168  <DocSecurity>0</DocSecurity>
3169  <ScaleCrop>false</ScaleCrop>
3170  <Company>${escape(ed.imprint || 'AI Book Generator')}</Company>
3171  <HyperlinksChanged>false</HyperlinksChanged>
3172  <AppVersion>12.3500</AppVersion>
3173</Properties>`;
3174
3175    const zip = new JSZip();
3176    zip.file('[Content_Types].xml', contentTypes);
3177    zip.file('_rels/.rels', rootRels);
3178    zip.file('docProps/core.xml', coreProps);
3179    zip.file('docProps/app.xml', appProps);
3180    zip.file('word/document.xml', docXml);
3181    zip.file('word/styles.xml', stylesXml);
3182    zip.file('word/header1.xml', headerXml);
3183    zip.file('word/headerFirst.xml', emptyHeaderXml);
3184    zip.file('word/footer1.xml', footerXml);
3185    zip.file('word/footerFirst.xml', emptyFooterXml);
3186    zip.file('word/_rels/document.xml.rels', docRels);
3187    imageRels.forEach((im) => {
3188        const b64 = im.dataUrl.slice(im.dataUrl.indexOf(',') + 1);
3189        zip.file(`word/media/img${im.rid.slice(5)}.${im.ext}`, b64, { base64: true });
3190    });
3191
3192    const blob = await zip.generateAsync({ type: 'blob', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
3193    const url = URL.createObjectURL(blob);
3194    const a = document.createElement('a');
3195    const filename = `${(bookTitle || 'book').replace(/[^a-z0-9]/gi, '_').toLowerCase()}.docx`;
3196    a.href = url; a.download = filename;
3197    document.body.appendChild(a); a.click(); document.body.removeChild(a);
3198    URL.revokeObjectURL(url);
3199}
3200async function getExportAuthor() {
3201    let authorText = 'Created with AI Book Generator';
3202    try {
3203        const creator = await window.websim?.getCreatedBy?.();
3204        if (creator && creator.username) authorText = `Created by @${creator.username}`;
3205    } catch (_) {}
3206    return authorText;
3207}
3208
3209function getPrefaceLabel() {
3210    const lang = (document.documentElement.getAttribute('lang') || 'en').toLowerCase();
3211    const map = { en: 'Preface', de: 'Vorwort', fr: 'Préface', es: 'Prefacio', it: 'Prefazione', ja: 'まえがき', 'zh-cn': '前言', ru: 'Предисловие' };
3212    return map[lang] || 'Preface';
3213}
3214
3215function getAboutAuthorLabel() {
3216    const lang = (document.documentElement.getAttribute('lang') || 'en').toLowerCase();
3217    const map = { en: 'About the Author', de: 'Über den Autor', fr: "À propos de l'auteur", es: 'Sobre el autor', it: "Sull'autore", ja: '著者について', 'zh-cn': '关于作者', ru: 'Об авторе' };
3218    return map[lang] || 'About the Author';
3219}
3220
3221/**
3222 * Exports the book to JSON format (uses existing saveBookData).
3223 */
3224async function exportToJSON(statusMessage) {
3225    // Get DOM elements
3226    const inputText = document.getElementById('input-text');
3227    const languageSelect = document.getElementById('language-select');
3228    const title = document.querySelector('h1');
3229    const bookContent = document.getElementById('book-content');
3230    
3231    // Get settings elements
3232    const chapterCountInput = document.getElementById('chapter-count-input');
3233    const genreSelect = document.getElementById('genre-select');
3234    const styleSelect = document.getElementById('style-select');
3235    const chapterSizeSlider = document.getElementById('chapter-size-slider');
3236    
3237    // Get bookmark
3238    const bookmarkedChapterId = document.querySelector('.bookmarked-chapter')?.id || null;
3239    
3240    // Import saveBookData function dynamically to avoid circular imports
3241    const { saveBookData } = await import('./book-manager.js');
3242    
3243    // Get the data to save
3244    const dataToSave = saveBookData(
3245        inputText, languageSelect, title, bookContent, 
3246        chapterCountInput, genreSelect, styleSelect, chapterSizeSlider,
3247        bookmarkedChapterId
3248    );
3249
3250    // --- New: Add sources & links for each chapter, extract from DOM and append to chapter data ---
3251    if (bookContent) {
3252        const chapterDOMs = Array.from(bookContent.children).filter(el => el.classList.contains('chapter'));
3253        dataToSave.chapters.forEach((chapterData, i) => {
3254            const dom = chapterDOMs[i];
3255            if (!dom) return;
3256            const contentDiv = dom.querySelector('.chapter-content');
3257            if (!contentDiv) return;
3258            const sourcesContainer = contentDiv.querySelector('.chapter-sources-container');
3259            if (sourcesContainer) {
3260                // Extract all links
3261                const sources = [];
3262                sourcesContainer.querySelectorAll('.chapter-source-link').forEach(linkDiv => {
3263                    const a = linkDiv.querySelector('a');
3264                    const title = a ? a.textContent : '';
3265                    const url = a ? a.href : '';
3266                    const type = linkDiv.querySelector('span')?.textContent || '';
3267                    const desc = linkDiv.querySelector('span + span')?.textContent || '';
3268                    sources.push({
3269                        title,
3270                        url,
3271                        type,
3272                        description: desc
3273                    });
3274                });
3275                chapterData.sources = sources;
3276            }
3277        });
3278    }
3279
3280    // ...existing code for download...
3281    const jsonString = JSON.stringify(dataToSave, null, 2);
3282    const blob = new Blob([jsonString], { type: "application/json" });
3283    const url = URL.createObjectURL(blob);
3284    
3285    const a = document.createElement('a');
3286    a.href = url;
3287    const safeFileName = (title.textContent || 'ai-book').replace(/[^a-z0-9._-]/gi, '_').toLowerCase();
3288    a.download = `${safeFileName}.json`;
3289    
3290    document.body.appendChild(a);
3291    a.click();
3292    document.body.removeChild(a);
3293    
3294    URL.revokeObjectURL(url);
3295}
3296
3297/**
3298 * Exports the book to PPTX format.
3299 */
3300async function exportToPPTX(bookTitle, chapters, settings) {
3301    const Pptx = await importPptxGen();
3302    const ppt = new Pptx(); ppt.layout = 'LAYOUT_16x9';
3303    const title = ppt.addSlide();
3304    title.addText(bookTitle, { x:0.5, y:3, w:12, fontSize:36, bold:true, align:'center' });
3305    chapters.forEach((ch, i) => {
3306        const s = ppt.addSlide();
3307        const h2 = ch.querySelector('h2')?.textContent || `Chapter ${i+1}`;
3308        s.addText(h2, { x:0.5, y:0.4, w:12, fontSize:24, bold:true });
3309        const bullets = [...ch.querySelectorAll('.chapter-content>p')].map(p=>p.textContent.trim())
3310            .filter(Boolean).slice(0, 8).map(t=>t.length>180? t.slice(0,177)+'…' : t);
3311        if (bullets.length) s.addText(bullets.map(t=>({ text:t, options:{ bullet:true, fontSize:14 } })),
3312                                      { x:0.7, y:1.2, w:11.3, h:5.5 });
3313    });
3314    await ppt.writeFile({ fileName: (bookTitle.replace(/[^a-z0-9]/gi,'_')||'book') + '.pptx' });
3315}
3316
3317/**
3318 * Escapes HTML special characters.
3319 * @param {string} text - The text to escape
3320 * @returns {string} Escaped HTML
3321 */
3322function escapeHtml(text) {
3323    const div = document.createElement('div');
3324    div.textContent = text;
3325    return div.innerHTML;
3326}
3327
3328/**
3329 * Escapes RTF special characters.
3330 * @param {string} text - The text to escape
3331 * @returns {string} Escaped RTF
3332 */
3333function escapeRtf(text) {
3334    return Array.from(text).map(ch => {
3335        const codePoint = ch.codePointAt(0);
3336        if (codePoint <= 0x7F) {
3337            if (ch === '\\') return '\\\\';
3338            if (ch === '{') return '\\{';
3339            if (ch === '}') return '\\}';
3340            return ch;
3341        }
3342        const signed = codePoint > 0x7FFF ? codePoint - 0x10000 : codePoint;
3343        return `\\u${signed}?`;
3344    }).join('');
3345}
3346
3347/**
3348 * Imports PptxGenJS library
3349 */
3350async function importPptxGen() {
3351    if (window.PptxGenJS) return window.PptxGenJS;
3352    await new Promise((r, j) => {
3353        const s = document.createElement('script');
3354        s.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/pptxgen.bundle.js';
3355        s.onload = r; s.onerror = j; document.head.appendChild(s);
3356    });
3357    return window.PptxGenJS;
3358}
3359
3360function getTocLabelForExport() {
3361    const sel = document.getElementById('language-select');
3362    const lang = sel ? sel.value : (document.documentElement.getAttribute('lang') || navigator.language || 'en');
3363    const map = { en:'Table of Contents', de:'Inhaltsverzeichnis', fr:'Table des matières', es:'Índice', it:'Indice', ja:'目次', 'zh-CN':'目录', ru:'Оглавление' };
3364    const l = (lang || 'en').toLowerCase();
3365    const key = l.startsWith('zh') ? 'zh-CN' : l.startsWith('ru') ? 'ru' : l.startsWith('ja') ? 'ja' : l.startsWith('de') ? 'de' : l.startsWith('fr') ? 'fr' : l.startsWith('es') ? 'es' : l.startsWith('it') ? 'it' : 'en';
3366    return map[key];
3367}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.