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">×</button> 122 <h2>Export & 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 & 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 & 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 & 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 & 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">×</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 © ${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, '&') 2716 .replace(/</g, '<') 2717 .replace(/>/g, '>') 2718 .replace(/"/g, '"') 2719 .replace(/'/g, '''); 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.