# Table of Contents Module # Contains functions to generate TOC AFTER content is placed with real page numbers import re import os import sys from typing import List, Dict, Any # Add the parent directory to sys.path to import from models.py and constants.py sys.path.append(os.path.dirname(os.path.dirname(__file__))) from models import ExportSection from constants import SECTION_ORDER def extract_first_heading_from_content(content: str) -> str: """Extract the first heading from markdown content""" if not content: return "" lines = content.split('\n') for line in lines: line = line.strip() if not line: continue # Check for markdown headings (# ## ### etc.) if line.startswith('#'): # Extract heading text without the # symbols heading_text = re.sub(r'^#+\s*', '', line).strip() if heading_text: return heading_text # Check for bold headings (**text**) elif line.startswith('**') and line.endswith('**') and len(line) > 4: # Extract text from bold markers heading_text = line[2:-2].strip() if heading_text: return heading_text return "" def generate_table_of_contents_after_content(sections: List[ExportSection], section_page_map: Dict[str, int], business_idea: str) -> List[Dict[str, Any]]: """Generate TOC AFTER all content is placed, using REAL page numbers from section_page_map This ensures accurate page numbers based on actual content placement""" toc_items = [] # Create a mapping from section titles to section objects for easy lookup section_map = {section.title: section for section in sections} # Add business plan sections in the predefined SECTION_ORDER for i, expected_title in enumerate(SECTION_ORDER): # Find the corresponding section - try multiple matching strategies section = None # Strategy 1: Exact title match section = section_map.get(expected_title) # Strategy 2: Case-insensitive match if not section: for section_title, section_obj in section_map.items(): if section_title.lower() == expected_title.lower(): section = section_obj break # Strategy 3: Partial match (e.g., "Company Profile" matches "Company Profile Section") if not section: for section_title, section_obj in section_map.items(): if (expected_title.lower() in section_title.lower() or section_title.lower() in expected_title.lower()): section = section_obj break # Strategy 4: Key-based match (remove 'prompt_' prefix and match) if not section: for section_obj in sections: if section_obj.key: key_clean = section_obj.key.replace('prompt_', '') if section_obj.key.startswith('prompt_') else section_obj.key if key_clean.lower() == expected_title.lower().replace(' ', '').replace('&', ''): section = section_obj break # Only add sections that have actual content AND real page numbers - NO DUMMY DATA has_content = bool(section and section.content and section.content.strip()) if has_content: # Get the REAL page number from section_page_map real_page = section_page_map.get(section.title, 0) if real_page > 0: # Only add if we have a real page number # Extract the actual heading from markdown content actual_heading = extract_first_heading_from_content(section.content) # Use the actual heading from markdown content, fallback to expected title toc_title = actual_heading if actual_heading else expected_title toc_items.append({ "title": toc_title, # Use actual heading from markdown content "level": 1, "page": real_page, # REAL page number from actual content placement "section_type": "section", "has_content": True, "section_obj": section # Store reference to actual section }) return toc_items def add_table_of_contents_page(pdf, toc_items: List[Dict[str, Any]], business_idea: str): """Add a professional table of contents page with REAL page numbers""" # TOC items are already in the correct order and have real page numbers sorted_toc_items = toc_items # Professional color scheme - Refined for bank/investor presentation primary_color = (0, 0, 0) # Black - for headings accent_color = (75, 0, 130) # Navy purple - for accent lines and highlights text_color = (51, 51, 51) # Dark gray - for body text light_gray = (221, 221, 221) # Lighter gray - for subtle lines (#DDDDDD) navy_color = (25, 25, 112) # Navy blue - for TOC and emphasis # Page title - more compact pdf.set_font("Arial", "B", 20) # Reduced from 24 to 20 pdf.set_text_color(*primary_color) pdf.set_xy(25, 20) # Reduced from 25 to 20 pdf.cell(0, 15, "Table of Contents", ln=True) # Reduced from 18 to 15 # Calculate text width and make line autofit text_width = pdf.get_string_width("Table of Contents") line_width = min(text_width + 12, 160) # Add 12mm padding, max 160mm # Removed purple accent line under TOC title # TOC content - optimized for single page layout y_position = 50 # Reduced from 60 to 50 pages_used = 1 for item in sorted_toc_items: # Only add new page if absolutely necessary (very close to bottom) if y_position > 290: # Very close to bottom margin pdf.add_page() y_position = 25 pages_used += 1 # Indent based on level - better indentation indent = 0 if item["level"] == 1 else 20 # Increased indent for subsections # Font and color based on level - ALL sections same color if item["level"] == 1: pdf.set_font("Arial", "B", 12) pdf.set_text_color(*navy_color) # Navy blue for all main entries else: pdf.set_font("Arial", "", 10) pdf.set_text_color(*text_color) # Title with hyperlink title_width = pdf.get_string_width(item["title"]) pdf.set_xy(25 + indent, y_position) try: # FPDF2 hyperlinks: test both 0-based and 1-based indexing # First try 1-based (no subtraction) target_page = item["page"] # Try different FPDF2 link methods if hasattr(pdf, 'add_link'): link_id = pdf.add_link() pdf.set_link(link_id, page=target_page) pdf.link(25 + indent, y_position, title_width, 8, link_id) else: pdf.link(25 + indent, y_position, title_width, 8, dest=target_page) except: pass # If hyperlink fails, continue without it pdf.cell(title_width, 8, item["title"], ln=False) # Dots leading to page number pdf.set_font("Arial", "", 8) pdf.set_text_color(128, 128, 128) # Medium gray for dots # Calculate dots position title_width = pdf.get_string_width(item["title"]) dots_start = 25 + indent + title_width + 5 dots_end = 160 # Draw dots for x in range(int(dots_start), int(dots_end), 3): pdf.set_xy(x, y_position + 3) pdf.cell(1, 1, ".", ln=False) # Page number with hyperlink - NOW USING REAL PAGE NUMBERS page_number_width = pdf.get_string_width(str(item["page"])) pdf.set_xy(160, y_position) try: # FPDF2 hyperlinks: test both 0-based and 1-based indexing # First try 1-based (no subtraction) target_page = item["page"] # Try different FPDF2 link methods if hasattr(pdf, 'add_link'): link_id = pdf.add_link() pdf.set_link(link_id, page=target_page) pdf.link(160, y_position, page_number_width, 8, link_id) else: pdf.link(160, y_position, page_number_width, 8, dest=target_page) except: pass # If hyperlink fails, continue without it pdf.cell(0, 8, str(item["page"]), ln=False) # Reduced spacing between items for better page utilization y_position += 8 # Reduced from 10 to 8 for even tighter spacing # Add some spacing at the end pdf.ln(20)