Spaces:
Runtime error
Runtime error
File size: 8,903 Bytes
8126b08 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | # 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) |