File size: 14,379 Bytes
2d2c483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
import re
from typing import List, Dict, Any

def calculate_group_height(pdf, group: Dict[str, Any]) -> int:
    """Calculate the estimated height needed for a complete group"""
    subheading_type = group['subheading_type']
    content_lines = group['content'].split('\n') if group['content'] else []
    
    # Base height for subheading
    if subheading_type == 'h1':
        subheading_height = 25  # 20pt font + spacing
    elif subheading_type == 'h2':
        subheading_height = 20  # 16pt font + underline + spacing
    elif subheading_type == 'h3':
        subheading_height = 16  # 14pt font + spacing
    elif subheading_type == 'h4':
        subheading_height = 14  # 12pt font + spacing
    elif subheading_type == 'bold':
        subheading_height = 20  # 16pt font + spacing
    elif subheading_type == 'numbered':
        subheading_height = 20  # 16pt font + spacing
    else:
        subheading_height = 16
    
    # Calculate content height more accurately
    content_height = 0
    for line in content_lines:
        if line.strip():
            # More accurate line height calculation
            words = line.split()
            if len(words) > 0:
                # Estimate characters per line (roughly 80-90 chars per line)
                chars_per_line = 85
                estimated_lines = max(1, len(line) // chars_per_line + 1)
                line_height = estimated_lines * 8  # 8pt per line
                content_height += line_height
            else:
                content_height += 8  # Empty line
        else:
            content_height += 3  # Empty line spacing
    
    # Add spacing between groups
    group_spacing = 8
    
    total_height = subheading_height + content_height + group_spacing
    
    # print(f"DEBUG: Group '{group.get('subheading_clean', 'Unknown')[:30]}...' height calculation:")
    # print(f"  Subheading height: {subheading_height}")
    # print(f"  Content height: {content_height}")
    # print(f"  Group spacing: {group_spacing}")
    # print(f"  Total height: {total_height}")
    
    return total_height

def should_break_page_for_group(pdf, group: Dict[str, Any], current_y: int, page_height: int = 280) -> bool:
    """Determine if we should break to a new page for this group"""
    estimated_height = calculate_group_height(pdf, group)
    
    # print(f"DEBUG: should_break_page_for_group called")
    # print(f"DEBUG:   Subheading: '{group.get('subheading_clean', 'Unknown')[:30]}...'")
    # print(f"DEBUG:   Current Y: {current_y}")
    # print(f"DEBUG:   Estimated height: {estimated_height}")
    # print(f"DEBUG:   Page height: {page_height}")
    # print(f"DEBUG:   Available space: {page_height - current_y}")
    # print(f"DEBUG:   Will fit? {current_y + estimated_height <= page_height}")
    
    # CRITICAL FIX: Only break if the group absolutely won't fit
    # This prevents splitting subheadings from their content
    
    # If the group won't fit on current page, break
    if current_y + estimated_height > page_height:
        # print(f"DEBUG:   -> BREAKING: Group won't fit ({current_y} + {estimated_height} > {page_height})")
        return True
    
    # If we're very close to the end of the page (less than 50mm remaining) and group is significant, break
    if current_y > page_height - 50 and estimated_height > 30:
        # print(f"DEBUG:   -> BREAKING: Very close to end of page ({current_y} > {page_height - 50}) and group significant ({estimated_height} > 30)")
        return True
    
    # print(f"DEBUG:   -> NO BREAK: Group will fit on current page")
    return False

def clean_markdown_from_line(line: str) -> str:
    """Clean markdown formatting from a single line"""
    if not line:
        return ""
    
    cleaned = line
    
    # Remove bold, italic formatting
    cleaned = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned)  # Remove bold
    cleaned = re.sub(r'\*(.*?)\*', r'\1', cleaned)  # Remove italic
    cleaned = re.sub(r'`(.*?)`', r'\1', cleaned)  # Remove code
    cleaned = re.sub(r'~~(.*?)~~', r'\1', cleaned)  # Remove strikethrough
    
    # Remove emphasis markers
    cleaned = re.sub(r'_{1,2}(.*?)_{1,2}', r'\1', cleaned)  # Remove underscores
    cleaned = re.sub(r'\*{1,2}(.*?)\*{1,2}', r'\1', cleaned)  # Remove asterisks
    
    # Remove links (keep text)
    cleaned = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', cleaned)  # Remove links, keep text
    
    # Remove any remaining HTML-like tags
    cleaned = re.sub(r'<[^>]+>', '', cleaned)
    
    # CRITICAL FIX: Replace multiple spaces with single space to prevent double spaces
    cleaned = re.sub(r' +', ' ', cleaned)  # Replace multiple spaces with single space
    
    return cleaned.strip()

def parse_content_into_groups(content: str) -> List[Dict[str, Any]]:
    """Parse markdown content into subheading-content groups"""
    if not content:
        return []
    
    # print(f"DEBUG: parse_content_into_groups called with content length: {len(content)}")
    
    lines = content.split('\n')
    groups = []
    current_group = None
    
    for i, line in enumerate(lines):
        line = line.strip()
        if not line:
            if current_group:
                current_group['content_lines'].append('')  # Preserve empty lines
            continue
        
        # Check if this is a heading (H1, H2, H3, H4, or numbered)
        is_heading = (
            line.startswith('# ') or  # H1
            (line.startswith('## ') and not line.startswith('### ')) or  # H2
            (line.startswith('### ') and not line.startswith('#### ')) or  # H3
            (line.startswith('#### ') and not line.startswith('##### ')) or  # H4
            re.match(r'^\d+\.\d+\s+', line)  # Numbered subheadings like "3.6 SWOT Analysis"
        )
        
        # Check if this is bold text
        is_bold_text = (
            (line.startswith('**') and line.endswith('**') and len(line) > 4) or  # Bold headings
            (re.match(r'^\*\*.*?\*\*:', line) and len(line) > 4)  # Bold text before colon as heading
        )

        # Check if this is a bold list item (e.g., "- **Item**")
        is_bold_list_item = re.match(r'^[-*]\s*\*\*(.*?)\*\*$', line)
        
        # Special handling for bold text following numbered subheadings
        should_create_new_group = False
        
        if is_heading:
            # Always create new group for regular headings
            should_create_new_group = True
            # print(f"DEBUG: Line {i+1} '{line[:50]}...' - Regular heading, creating new group")
        elif is_bold_text:
            # Check if we should treat this bold text as content for a numbered subheading
            if groups and groups[-1].get('subheading_type') == 'h3':
                # Check if the previous group's subheading looks like a numbered subheading
                previous_subheading = groups[-1].get('subheading', '')
                if re.match(r'^\d+\.\d+\s+', previous_subheading):
                    # This is content for a numbered subheading, add to previous group
                    groups[-1]['content_lines'].append(line)
                    # print(f"DEBUG: Line {i+1} '{line[:50]}...' - Adding to previous numbered subheading '{previous_subheading[:30]}...'")
                    continue  # Skip creating new group
                else:
                    # Not a numbered subheading, treat as new heading
                    should_create_new_group = True
                    # print(f"DEBUG: Line {i+1} '{line[:50]}...' - Treating as NEW HEADING (not numbered subheading)")
            else:
                # No previous group or not a numbered subheading, treat as new heading
                should_create_new_group = True
                # print(f"DEBUG: Line {i+1} '{line[:50]}...' - Treating as NEW HEADING (no numbered subheading)")
        elif is_bold_list_item:
            # If it's a bold list item, treat it as a new heading
            should_create_new_group = True
            # print(f"DEBUG: Line {i+1} '{line[:50]}...' - Bold list item, creating new group")
        
        if should_create_new_group:
            # Save previous group if it exists
            if current_group:
                current_group['content'] = '\n'.join(current_group['content_lines'])
                groups.append(current_group)
                # print(f"DEBUG: Saved group '{current_group.get('subheading_clean', 'Unknown')[:30]}...' with {len(current_group['content_lines'])} content lines")
            
            # Clean markdown symbols BEFORE determining type and storing
            clean_text = line
            heading_type = 'text'
            
            if line.startswith('# '):
                clean_text = line[2:].strip()  # Remove "# "
                heading_type = 'h1'
            elif line.startswith('## '):
                clean_text = line[3:].strip()  # Remove "## "
                heading_type = 'h2'
            elif line.startswith('### '):
                clean_text = line[4:].strip()  # Remove "### "
                heading_type = 'h3'
            elif line.startswith('#### '):
                clean_text = line[5:].strip()  # Remove "#### "
                heading_type = 'h4'
            elif re.match(r'^\d+\.\d+\s+', line):
                clean_text = line.strip()  # Keep numbered headings as-is
                heading_type = 'numbered'
            elif is_bold_text:
                # Remove ** markers from bold text
                clean_text = clean_markdown_from_line(line.strip())
                heading_type = 'bold'
            elif is_bold_list_item:
                # Extract the bold text from list item and treat as H3 subheading
                # Remove the list marker and ** markers
                clean_text = re.sub(r'^[-*]\s*\*\*(.*?)\*\*', r'\1', line.strip())
                heading_type = 'h3'  # Treat as H3 subheading
            
            # print(f"DEBUG: Creating new group '{clean_text[:30]}...' (type: {heading_type})")
            
            # Start new group with cleaned text
            current_group = {
                'subheading': line,  # Keep original for reference
                'content_lines': [],
                'content': '',
                'subheading_clean': clean_text,  # Pre-cleaned text
                'subheading_type': heading_type
            }
        else:
            # Add to current group's content - clean the line
            if current_group:
                cleaned_line = clean_markdown_from_line(line)
                current_group['content_lines'].append(cleaned_line)
                if i < 5:  # Log first few content lines
                    # print(f"DEBUG: Added content line to group '{current_group.get('subheading_clean', 'Unknown')[:30]}...': '{cleaned_line[:50]}...'")
                    pass
            else:
                # Content before any subheading - create a default group
                if not groups or groups[-1].get('subheading') != '## Introduction':
                    cleaned_line = clean_markdown_from_line(line)
                    current_group = {
                        'subheading': '## Introduction',
                        'content_lines': [cleaned_line],
                        'content': '',
                        'subheading_clean': 'Introduction',
                        'subheading_type': 'h2'
                    }
    
    # Don't forget the last group
    if current_group:
        current_group['content'] = '\n'.join(current_group['content_lines'])
        groups.append(current_group)
        # print(f"DEBUG: Saved final group '{current_group.get('subheading_clean', 'Unknown')[:30]}...' with {len(current_group['content_lines'])} content lines")
    
    # Add metadata to each group
    for i, group in enumerate(groups):
        group['group_id'] = f"group_{i}"
        group['content_length'] = len(group['content'])
        group['word_count'] = len(group['content'].split()) if group['content'] else 0
        group['has_charts'] = 'CHART' in group['content'].upper() if group['content'] else False
        group['line_count'] = len([line for line in group['content'].split('\n') if line.strip()])
    
    # print(f"DEBUG: Created {len(groups)} groups total")
    return groups

def extract_key_phrases(content: str) -> List[tuple]:
    """Extract key phrases from content for grouping metadata"""
    if not content:
        return []
    
    # Simple key phrase extraction
    words = re.findall(r'\b\w+\b', content.lower())
    word_freq = {}
    
    for word in words:
        if len(word) > 3:  # Only words longer than 3 characters
            word_freq[word] = word_freq.get(word, 0) + 1
    
    # Return top 5 most frequent words
    return sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:5]

def get_content_groups_info(content: str) -> Dict[str, Any]:
    """Get detailed information about content groups for analysis and debugging"""
    if not content:
        return {"groups": [], "summary": {"total_groups": 0, "total_content_length": 0}}
    
    groups = parse_content_into_groups(content)
    
    # Add key phrases to each group
    for group in groups:
        group['key_phrases'] = extract_key_phrases(group['content'])
    
    # Calculate summary statistics
    total_content_length = sum(group['content_length'] for group in groups)
    total_word_count = sum(group['word_count'] for group in groups)
    groups_with_charts = sum(1 for group in groups if group['has_charts'])
    
    summary = {
        "total_groups": len(groups),
        "total_content_length": total_content_length,
        "total_word_count": total_word_count,
        "groups_with_charts": groups_with_charts,
        "average_group_size": total_content_length / len(groups) if groups else 0,
        "largest_group": max(groups, key=lambda g: g['content_length'])['subheading_clean'] if groups else None,
        "smallest_group": min(groups, key=lambda g: g['content_length'])['subheading_clean'] if groups else None
    }
    
    return {
        "groups": groups,
        "summary": summary
    }