Spaces:
Sleeping
Sleeping
| # 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) |