doctra 0.2.0__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1235 @@
1
+ from __future__ import annotations
2
+ import os
3
+ import re
4
+ import base64
5
+ from typing import List, Dict, Any
6
+ from markdown_it import MarkdownIt
7
+
8
+
9
+ def _process_image_paths(md_content: str, out_dir: str) -> str:
10
+ """
11
+ Process image paths in markdown content to ensure they work in HTML.
12
+ Converts relative paths to base64 embedded images for better portability.
13
+
14
+ :param md_content: The markdown content with image references
15
+ :param out_dir: The output directory where images are stored
16
+ :return: Processed markdown content with embedded images
17
+ """
18
+ def replace_image(match):
19
+ caption = match.group(1)
20
+ img_path = match.group(2)
21
+
22
+ # Convert relative path to absolute path
23
+ if not os.path.isabs(img_path):
24
+ abs_img_path = os.path.join(out_dir, img_path)
25
+ else:
26
+ abs_img_path = img_path
27
+
28
+ # Check if image exists
29
+ if os.path.exists(abs_img_path):
30
+ try:
31
+ # Read image and convert to base64
32
+ with open(abs_img_path, 'rb') as f:
33
+ img_data = f.read()
34
+ img_ext = os.path.splitext(abs_img_path)[1].lower()
35
+
36
+ # Determine MIME type
37
+ if img_ext in ['.jpg', '.jpeg']:
38
+ mime_type = 'image/jpeg'
39
+ elif img_ext == '.png':
40
+ mime_type = 'image/png'
41
+ elif img_ext == '.gif':
42
+ mime_type = 'image/gif'
43
+ elif img_ext == '.webp':
44
+ mime_type = 'image/webp'
45
+ else:
46
+ mime_type = 'image/jpeg' # fallback
47
+
48
+ # Encode to base64
49
+ b64_data = base64.b64encode(img_data).decode('ascii')
50
+
51
+ # Return HTML img tag with base64 data
52
+ return f'<img src="data:{mime_type};base64,{b64_data}" alt="{caption}" />'
53
+ except Exception as e:
54
+ print(f"Warning: Could not process image {abs_img_path}: {e}")
55
+ return f'<div class="image-error">Image not found: {caption}</div>'
56
+ else:
57
+ print(f"Warning: Image file not found: {abs_img_path}")
58
+ return f'<div class="image-error">Image not found: {caption}</div>'
59
+
60
+ # Find all image references in markdown format ![caption](path)
61
+ pattern = r'!\[([^\]]*)\]\(([^)]+)\)'
62
+ processed_content = re.sub(pattern, replace_image, md_content)
63
+
64
+ return processed_content
65
+
66
+
67
+ def write_html(md_lines: List[str], out_dir: str, filename: str = "result.html") -> str:
68
+ """
69
+ Convert collected Markdown lines into a single HTML file and save it.
70
+
71
+ Converts Markdown content to HTML with proper styling, table support,
72
+ and code highlighting. Includes a modern, responsive design.
73
+
74
+ :param md_lines: List of markdown strings to join into a single file
75
+ :param out_dir: Directory where the HTML file will be saved
76
+ :param filename: Name of the HTML file (default: "result.html")
77
+ :return: The absolute path of the written HTML file
78
+ """
79
+ os.makedirs(out_dir, exist_ok=True)
80
+
81
+ # Join markdown lines and clean up excessive blank lines
82
+ md_content = "\n".join(md_lines).strip() + "\n"
83
+ md_content = re.sub(r"\n{3,}", "\n\n", md_content)
84
+
85
+ # Process image paths to convert relative paths to absolute paths or base64
86
+ md_content = _process_image_paths(md_content, out_dir)
87
+
88
+ # Convert markdown to HTML with markdown-it-py
89
+ md = MarkdownIt("commonmark", {"breaks": True, "html": True})
90
+
91
+ # Render markdown to HTML
92
+ html_body = md.render(md_content)
93
+
94
+ # Always apply table styling to ensure all tables are properly formatted
95
+ html_body = _add_table_styling(html_body)
96
+
97
+ # Create complete HTML document with modern styling
98
+ html_content = f"""<!DOCTYPE html>
99
+ <html lang="en">
100
+ <head>
101
+ <meta charset="UTF-8">
102
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
103
+ <title>Document Analysis Results</title>
104
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
105
+ <style>
106
+ {_get_css_styles()}
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle dark mode"></button>
111
+ <div class="container">
112
+ <header class="header">
113
+ <div class="header-content">
114
+ <div class="header-text">
115
+ <h1>Document Analysis Results</h1>
116
+ <p class="subtitle">Intelligent Document Processing & Analysis</p>
117
+ </div>
118
+ <div class="header-badge">
119
+ Generated by Doctra
120
+ </div>
121
+ </div>
122
+ </header>
123
+ <main class="content">
124
+ {html_body}
125
+ </main>
126
+ <footer class="footer">
127
+ <div class="footer-content">
128
+ <div class="footer-brand">Doctra</div>
129
+ <div class="footer-info">
130
+ <span>Intelligent Document Processing</span>
131
+ <a href="https://github.com/AdemBoukhris457/Doctra" target="_blank">GitHub</a>
132
+ </div>
133
+ </div>
134
+ </footer>
135
+ </div>
136
+ <script>
137
+ // Theme toggle functionality
138
+ function toggleTheme() {{
139
+ const body = document.body;
140
+ const currentTheme = body.getAttribute('data-theme');
141
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
142
+
143
+ body.setAttribute('data-theme', newTheme);
144
+ localStorage.setItem('doctra-theme', newTheme);
145
+
146
+ // Add smooth transition
147
+ body.style.transition = 'all 0.3s ease';
148
+ setTimeout(() => {{
149
+ body.style.transition = '';
150
+ }}, 300);
151
+ }}
152
+
153
+ // Load saved theme on page load
154
+ document.addEventListener('DOMContentLoaded', function() {{
155
+ const savedTheme = localStorage.getItem('doctra-theme') || 'light';
156
+ document.body.setAttribute('data-theme', savedTheme);
157
+ }});
158
+
159
+ // Add smooth scroll behavior
160
+ document.documentElement.style.scrollBehavior = 'smooth';
161
+
162
+ // Add loading animation
163
+ window.addEventListener('load', function() {{
164
+ document.body.style.opacity = '0';
165
+ document.body.style.transition = 'opacity 0.5s ease';
166
+ setTimeout(() => {{
167
+ document.body.style.opacity = '1';
168
+ }}, 100);
169
+ }});
170
+ </script>
171
+ </body>
172
+ </html>"""
173
+
174
+ html_path = os.path.join(out_dir, filename)
175
+ with open(html_path, "w", encoding="utf-8") as f:
176
+ f.write(html_content)
177
+
178
+ return os.path.abspath(html_path)
179
+
180
+
181
+ def write_structured_html(html_path: str, items: List[Dict[str, Any]]) -> str | None:
182
+ """
183
+ Write a list of structured data items into an HTML file with tables.
184
+
185
+ Each item becomes a separate section with styled tables. The function
186
+ handles data normalization and creates a responsive HTML layout.
187
+
188
+ :param html_path: Path where the HTML file will be saved
189
+ :param items: List of dictionaries, each containing:
190
+ - 'title': Section title (optional)
191
+ - 'headers': List of column headers (optional)
192
+ - 'rows': List of data rows (optional)
193
+ :return: Path to the written HTML file if successful, None if no items provided
194
+ """
195
+ if not items:
196
+ return None
197
+
198
+ # Filter out items that have no meaningful data
199
+ valid_items = []
200
+ for item in items:
201
+ headers = item.get("headers") or []
202
+ rows = item.get("rows") or []
203
+ # Keep items that have either headers or rows with data
204
+ if headers or (rows and any(
205
+ row for row in rows if any(cell for cell in row if cell is not None and str(cell).strip()))):
206
+ valid_items.append(item)
207
+
208
+ if not valid_items:
209
+ print("Warning: No valid items to write to HTML")
210
+ return None
211
+
212
+ os.makedirs(os.path.dirname(html_path) or ".", exist_ok=True)
213
+
214
+ # Generate HTML content
215
+ html_sections = []
216
+ for item in valid_items:
217
+ try:
218
+ title = item.get("title") or "Untitled"
219
+ headers = item.get("headers") or []
220
+ rows = item.get("rows") or []
221
+
222
+ # Normalize data to handle mismatched dimensions
223
+ normalized_headers, normalized_rows = _normalize_data(headers, rows)
224
+
225
+ if not normalized_rows and not normalized_headers:
226
+ print(f"Skipping empty item: {title}")
227
+ continue
228
+
229
+ # Create HTML table
230
+ table_html = _create_html_table(normalized_headers, normalized_rows)
231
+ section_html = f"""
232
+ <section class="data-section">
233
+ <h2 class="section-title">{_escape_html(title)}</h2>
234
+ {table_html}
235
+ </section>
236
+ """
237
+ html_sections.append(section_html)
238
+
239
+ except Exception as e:
240
+ print(f"Error processing item '{item.get('title', 'Unknown')}': {e}")
241
+ continue
242
+
243
+ if not html_sections:
244
+ print("Warning: No valid sections to write to HTML")
245
+ return None
246
+
247
+ # Create complete HTML document
248
+ html_content = f"""<!DOCTYPE html>
249
+ <html lang="en">
250
+ <head>
251
+ <meta charset="UTF-8">
252
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
253
+ <title>Structured Data Export</title>
254
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
255
+ <style>
256
+ {_get_css_styles()}
257
+ {_get_table_css_styles()}
258
+ </style>
259
+ </head>
260
+ <body>
261
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle dark mode"></button>
262
+ <div class="container">
263
+ <header class="header">
264
+ <div class="header-content">
265
+ <div class="header-text">
266
+ <h1>Structured Data Export</h1>
267
+ <p class="subtitle">Intelligent Document Processing & Analysis</p>
268
+ </div>
269
+ <div class="header-badge">
270
+ Generated by Doctra
271
+ </div>
272
+ </div>
273
+ </header>
274
+ <main class="content">
275
+ {''.join(html_sections)}
276
+ </main>
277
+ <footer class="footer">
278
+ <div class="footer-content">
279
+ <div class="footer-brand">Doctra</div>
280
+ <div class="footer-info">
281
+ <span>Intelligent Document Processing</span>
282
+ <a href="https://github.com/AdemBoukhris457/Doctra" target="_blank">GitHub</a>
283
+ </div>
284
+ </div>
285
+ </footer>
286
+ </div>
287
+ <script>
288
+ // Theme toggle functionality
289
+ function toggleTheme() {{
290
+ const body = document.body;
291
+ const currentTheme = body.getAttribute('data-theme');
292
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
293
+
294
+ body.setAttribute('data-theme', newTheme);
295
+ localStorage.setItem('doctra-theme', newTheme);
296
+
297
+ // Add smooth transition
298
+ body.style.transition = 'all 0.3s ease';
299
+ setTimeout(() => {{
300
+ body.style.transition = '';
301
+ }}, 300);
302
+ }}
303
+
304
+ // Load saved theme on page load
305
+ document.addEventListener('DOMContentLoaded', function() {{
306
+ const savedTheme = localStorage.getItem('doctra-theme') || 'light';
307
+ document.body.setAttribute('data-theme', savedTheme);
308
+ }});
309
+
310
+ // Add smooth scroll behavior
311
+ document.documentElement.style.scrollBehavior = 'smooth';
312
+
313
+ // Add loading animation
314
+ window.addEventListener('load', function() {{
315
+ document.body.style.opacity = '0';
316
+ document.body.style.transition = 'opacity 0.5s ease';
317
+ setTimeout(() => {{
318
+ document.body.style.opacity = '1';
319
+ }}, 100);
320
+ }});
321
+
322
+ // Add table interaction enhancements
323
+ document.addEventListener('DOMContentLoaded', function() {{
324
+ const tables = document.querySelectorAll('.data-table');
325
+ tables.forEach(table => {{
326
+ table.addEventListener('mouseenter', function() {{
327
+ this.style.transform = 'scale(1.01)';
328
+ }});
329
+ table.addEventListener('mouseleave', function() {{
330
+ this.style.transform = 'scale(1)';
331
+ }});
332
+ }});
333
+ }});
334
+ </script>
335
+ </body>
336
+ </html>"""
337
+
338
+ with open(html_path, "w", encoding="utf-8") as f:
339
+ f.write(html_content)
340
+
341
+ return html_path
342
+
343
+
344
+ def _normalize_data(headers: List[str], rows: List[List]) -> tuple[List[str], List[List]]:
345
+ """
346
+ Normalize headers and rows to ensure consistent dimensions.
347
+
348
+ :param headers: List of column headers
349
+ :param rows: List of data rows
350
+ :return: Tuple of (normalized_headers, normalized_rows)
351
+ """
352
+ if not rows:
353
+ return headers, []
354
+
355
+ # Find the maximum number of columns across all rows
356
+ max_cols = max(len(row) for row in rows) if rows else 0
357
+
358
+ # If we have headers, use them as the basis, otherwise use max columns
359
+ if headers:
360
+ target_cols = max(len(headers), max_cols)
361
+ else:
362
+ target_cols = max_cols
363
+ headers = [f"Column_{i + 1}" for i in range(target_cols)]
364
+
365
+ # Normalize headers: pad with generic names if too short, truncate if too long
366
+ normalized_headers = list(headers)
367
+ while len(normalized_headers) < target_cols:
368
+ normalized_headers.append(f"Column_{len(normalized_headers) + 1}")
369
+ normalized_headers = normalized_headers[:target_cols]
370
+
371
+ # Normalize rows: pad with None if too short, truncate if too long
372
+ normalized_rows = []
373
+ for row in rows:
374
+ normalized_row = list(row)
375
+ while len(normalized_row) < target_cols:
376
+ normalized_row.append(None)
377
+ normalized_rows.append(normalized_row[:target_cols])
378
+
379
+ return normalized_headers, normalized_rows
380
+
381
+
382
+ def _create_html_table(headers: List[str], rows: List[List]) -> str:
383
+ """
384
+ Create an HTML table from headers and rows.
385
+
386
+ :param headers: List of column headers
387
+ :param rows: List of data rows
388
+ :return: HTML table string
389
+ """
390
+ if not headers and not rows:
391
+ return "<p class='no-data'>No data available</p>"
392
+
393
+ # Create table header
394
+ header_html = ""
395
+ if headers:
396
+ header_cells = "".join(f"<th>{_escape_html(str(header))}</th>" for header in headers)
397
+ header_html = f"<thead><tr>{header_cells}</tr></thead>"
398
+
399
+ # Create table body
400
+ body_rows = []
401
+ for row in rows:
402
+ cells = "".join(f"<td>{_escape_html(str(cell) if cell is not None else '')}</td>" for cell in row)
403
+ body_rows.append(f"<tr>{cells}</tr>")
404
+
405
+ body_html = f"<tbody>{''.join(body_rows)}</tbody>" if body_rows else ""
406
+
407
+ return f"""
408
+ <div class="table-container">
409
+ <table class="data-table">
410
+ {header_html}
411
+ {body_html}
412
+ </table>
413
+ </div>
414
+ """
415
+
416
+
417
+ def _add_table_styling(html_content: str) -> str:
418
+ """
419
+ Add table styling wrapper to HTML content.
420
+
421
+ :param html_content: HTML content that may contain tables
422
+ :return: HTML content with table styling
423
+ """
424
+ # Wrap tables in a styled container - handle both <table> and <table class="...">
425
+ html_content = re.sub(
426
+ r'<table(?:\s+[^>]*)?>',
427
+ '<div class="table-container"><table class="markdown-table">',
428
+ html_content
429
+ )
430
+ html_content = re.sub(
431
+ r'</table>',
432
+ '</table></div>',
433
+ html_content
434
+ )
435
+ return html_content
436
+
437
+
438
+ def _escape_html(text: str) -> str:
439
+ """
440
+ Escape HTML special characters.
441
+
442
+ :param text: Text to escape
443
+ :return: Escaped text
444
+ """
445
+ if not text:
446
+ return ""
447
+
448
+ text = str(text)
449
+ text = text.replace("&", "&amp;")
450
+ text = text.replace("<", "&lt;")
451
+ text = text.replace(">", "&gt;")
452
+ text = text.replace('"', "&quot;")
453
+ text = text.replace("'", "&#x27;")
454
+ return text
455
+
456
+
457
+ def _get_css_styles() -> str:
458
+ """Get CSS styles for the HTML document."""
459
+ return """
460
+ :root {
461
+ --primary-color: #1e40af;
462
+ --secondary-color: #1e3a8a;
463
+ --accent-color: #3b82f6;
464
+ --success-color: #059669;
465
+ --warning-color: #d97706;
466
+ --error-color: #dc2626;
467
+ --text-color: #111827;
468
+ --text-light: #6b7280;
469
+ --text-muted: #9ca3af;
470
+ --bg-color: #f9fafb;
471
+ --card-bg: #ffffff;
472
+ --border-color: #e5e7eb;
473
+ --border-light: #f3f4f6;
474
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
475
+ --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.06);
476
+ --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05);
477
+ --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04);
478
+ --border-radius: 8px;
479
+ --border-radius-lg: 12px;
480
+ --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
481
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
482
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
483
+ }
484
+
485
+ * {
486
+ margin: 0;
487
+ padding: 0;
488
+ box-sizing: border-box;
489
+ }
490
+
491
+ html {
492
+ scroll-behavior: smooth;
493
+ }
494
+
495
+ body {
496
+ font-family: var(--font-family);
497
+ line-height: 1.6;
498
+ color: var(--text-color);
499
+ background: var(--bg-color);
500
+ min-height: 100vh;
501
+ font-size: 16px;
502
+ -webkit-font-smoothing: antialiased;
503
+ -moz-osx-font-smoothing: grayscale;
504
+ }
505
+
506
+ .container {
507
+ max-width: 1200px;
508
+ margin: 0 auto;
509
+ padding: 0;
510
+ background: var(--card-bg);
511
+ min-height: 100vh;
512
+ box-shadow: var(--shadow-xl);
513
+ }
514
+
515
+ .header {
516
+ background: var(--primary-color);
517
+ color: white;
518
+ padding: 2rem 3rem;
519
+ border-bottom: 4px solid var(--accent-color);
520
+ position: relative;
521
+ }
522
+
523
+ .header::before {
524
+ content: '';
525
+ position: absolute;
526
+ top: 0;
527
+ left: 0;
528
+ right: 0;
529
+ bottom: 0;
530
+ background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
531
+ pointer-events: none;
532
+ }
533
+
534
+ .header-content {
535
+ position: relative;
536
+ z-index: 1;
537
+ display: flex;
538
+ align-items: center;
539
+ justify-content: space-between;
540
+ }
541
+
542
+ .header-text {
543
+ flex: 1;
544
+ }
545
+
546
+ .header h1 {
547
+ font-size: 2.5rem;
548
+ margin-bottom: 0.5rem;
549
+ font-weight: 700;
550
+ letter-spacing: -0.025em;
551
+ }
552
+
553
+ .subtitle {
554
+ font-size: 1.1rem;
555
+ opacity: 0.9;
556
+ font-weight: 400;
557
+ color: rgba(255, 255, 255, 0.8);
558
+ }
559
+
560
+ .header-badge {
561
+ background: rgba(255, 255, 255, 0.2);
562
+ padding: 0.5rem 1rem;
563
+ border-radius: var(--border-radius);
564
+ font-size: 0.9rem;
565
+ font-weight: 500;
566
+ backdrop-filter: blur(10px);
567
+ border: 1px solid rgba(255, 255, 255, 0.3);
568
+ }
569
+
570
+ .content {
571
+ padding: 3rem;
572
+ background: var(--card-bg);
573
+ }
574
+
575
+ .content h1, .content h2, .content h3, .content h4, .content h5, .content h6 {
576
+ color: var(--text-color);
577
+ margin-top: 2rem;
578
+ margin-bottom: 1rem;
579
+ font-weight: 600;
580
+ line-height: 1.3;
581
+ }
582
+
583
+ .content h1 {
584
+ font-size: 2.25rem;
585
+ font-weight: 700;
586
+ color: var(--primary-color);
587
+ border-bottom: 3px solid var(--accent-color);
588
+ padding-bottom: 0.5rem;
589
+ margin-bottom: 1.5rem;
590
+ }
591
+ .content h2 {
592
+ font-size: 1.875rem;
593
+ font-weight: 600;
594
+ color: var(--text-color);
595
+ margin-top: 2.5rem;
596
+ margin-bottom: 1rem;
597
+ position: relative;
598
+ }
599
+ .content h2::before {
600
+ content: '';
601
+ position: absolute;
602
+ left: -1rem;
603
+ top: 50%;
604
+ transform: translateY(-50%);
605
+ width: 4px;
606
+ height: 1.5rem;
607
+ background: var(--accent-color);
608
+ border-radius: 2px;
609
+ }
610
+ .content h3 {
611
+ font-size: 1.5rem;
612
+ font-weight: 600;
613
+ color: var(--text-color);
614
+ margin-top: 2rem;
615
+ }
616
+ .content h4 {
617
+ font-size: 1.25rem;
618
+ font-weight: 600;
619
+ color: var(--text-light);
620
+ margin-top: 1.5rem;
621
+ }
622
+
623
+ .content p {
624
+ margin-bottom: 1.25rem;
625
+ font-size: 1rem;
626
+ line-height: 1.7;
627
+ color: var(--text-color);
628
+ }
629
+
630
+ .content img {
631
+ max-width: 100%;
632
+ height: auto;
633
+ border-radius: var(--border-radius);
634
+ box-shadow: var(--shadow-md);
635
+ margin: 1.5rem 0;
636
+ transition: var(--transition);
637
+ border: 1px solid var(--border-color);
638
+ }
639
+
640
+ .content img:hover {
641
+ transform: translateY(-2px);
642
+ box-shadow: var(--shadow-lg);
643
+ }
644
+
645
+ .image-error {
646
+ background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
647
+ border: 2px dashed #f44336;
648
+ border-radius: var(--border-radius);
649
+ padding: 1rem;
650
+ margin: 1rem 0;
651
+ text-align: center;
652
+ color: #d32f2f;
653
+ font-style: italic;
654
+ font-weight: 500;
655
+ }
656
+
657
+ .content pre {
658
+ background: #f8fafc;
659
+ border: 1px solid var(--border-color);
660
+ border-radius: var(--border-radius);
661
+ padding: 1.5rem;
662
+ overflow-x: auto;
663
+ margin: 1.5rem 0;
664
+ box-shadow: var(--shadow);
665
+ position: relative;
666
+ font-family: var(--font-mono);
667
+ }
668
+
669
+ .content pre::before {
670
+ content: '';
671
+ position: absolute;
672
+ top: 0;
673
+ left: 0;
674
+ right: 0;
675
+ height: 3px;
676
+ background: var(--accent-color);
677
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
678
+ }
679
+
680
+ .content code {
681
+ background: #f1f5f9;
682
+ padding: 0.25rem 0.5rem;
683
+ border-radius: 4px;
684
+ font-family: var(--font-mono);
685
+ font-size: 0.875em;
686
+ font-weight: 500;
687
+ color: var(--primary-color);
688
+ border: 1px solid var(--border-light);
689
+ }
690
+
691
+ .content pre code {
692
+ background: none;
693
+ padding: 0;
694
+ border: none;
695
+ color: var(--text-color);
696
+ }
697
+
698
+ .content blockquote {
699
+ border-left: 4px solid var(--accent-color);
700
+ margin: 1.5rem 0;
701
+ padding: 1rem 1.5rem;
702
+ color: var(--text-light);
703
+ font-style: italic;
704
+ background: #f8fafc;
705
+ border-radius: 0 var(--border-radius) var(--border-radius) 0;
706
+ position: relative;
707
+ }
708
+
709
+ .content blockquote::before {
710
+ content: '"';
711
+ font-size: 2rem;
712
+ color: var(--accent-color);
713
+ position: absolute;
714
+ top: -0.25rem;
715
+ left: 0.75rem;
716
+ opacity: 0.5;
717
+ font-family: serif;
718
+ }
719
+
720
+ .content ul, .content ol {
721
+ margin: 1rem 0;
722
+ padding-left: 1.5rem;
723
+ }
724
+
725
+ .content li {
726
+ margin-bottom: 0.5rem;
727
+ font-size: 1rem;
728
+ line-height: 1.6;
729
+ }
730
+
731
+ .content ul li {
732
+ position: relative;
733
+ list-style: none;
734
+ }
735
+
736
+ .content ul li::before {
737
+ content: '•';
738
+ color: var(--accent-color);
739
+ font-weight: bold;
740
+ position: absolute;
741
+ left: -1rem;
742
+ }
743
+
744
+ .footer {
745
+ background: #f8fafc;
746
+ border-top: 1px solid var(--border-color);
747
+ padding: 2rem 3rem;
748
+ color: var(--text-light);
749
+ font-size: 0.9rem;
750
+ }
751
+
752
+ .footer-content {
753
+ display: flex;
754
+ justify-content: space-between;
755
+ align-items: center;
756
+ max-width: 1200px;
757
+ margin: 0 auto;
758
+ }
759
+
760
+ .footer a {
761
+ color: var(--primary-color);
762
+ text-decoration: none;
763
+ font-weight: 500;
764
+ transition: var(--transition);
765
+ }
766
+
767
+ .footer a:hover {
768
+ color: var(--accent-color);
769
+ text-decoration: underline;
770
+ }
771
+
772
+ .footer-info {
773
+ display: flex;
774
+ gap: 2rem;
775
+ align-items: center;
776
+ }
777
+
778
+ .footer-brand {
779
+ font-weight: 600;
780
+ color: var(--text-color);
781
+ }
782
+
783
+ /* Professional table styling */
784
+ .markdown-table, table {
785
+ width: 100%;
786
+ border-collapse: collapse;
787
+ margin: 1.5rem 0;
788
+ background: var(--card-bg);
789
+ border-radius: var(--border-radius);
790
+ overflow: hidden;
791
+ box-shadow: var(--shadow-md);
792
+ border: 1px solid var(--border-color);
793
+ }
794
+
795
+ .markdown-table th, .markdown-table td,
796
+ table th, table td {
797
+ padding: 0.75rem 1rem;
798
+ text-align: left;
799
+ border-bottom: 1px solid var(--border-light);
800
+ }
801
+
802
+ .markdown-table th, table th {
803
+ background: #f8fafc;
804
+ color: var(--text-color);
805
+ font-weight: 600;
806
+ font-size: 0.875rem;
807
+ text-transform: uppercase;
808
+ letter-spacing: 0.05em;
809
+ border-bottom: 2px solid var(--accent-color);
810
+ }
811
+
812
+ .markdown-table tr:nth-child(even), table tr:nth-child(even) {
813
+ background: #f8fafc;
814
+ }
815
+
816
+ .markdown-table tr:hover, table tr:hover {
817
+ background: #f1f5f9;
818
+ transition: var(--transition);
819
+ }
820
+
821
+ .markdown-table tr:last-child td, table tr:last-child td {
822
+ border-bottom: none;
823
+ }
824
+
825
+ /* Ensure all tables are wrapped in containers */
826
+ .table-container {
827
+ margin: 1.5rem 0;
828
+ overflow-x: auto;
829
+ border-radius: var(--border-radius);
830
+ box-shadow: var(--shadow-md);
831
+ }
832
+
833
+ .table-container table {
834
+ margin: 0;
835
+ box-shadow: none;
836
+ }
837
+
838
+ /* Professional dark mode toggle */
839
+ .theme-toggle {
840
+ position: fixed;
841
+ top: 1.5rem;
842
+ right: 1.5rem;
843
+ background: var(--card-bg);
844
+ border: 1px solid var(--border-color);
845
+ border-radius: var(--border-radius);
846
+ width: 44px;
847
+ height: 44px;
848
+ display: flex;
849
+ align-items: center;
850
+ justify-content: center;
851
+ cursor: pointer;
852
+ box-shadow: var(--shadow-md);
853
+ transition: var(--transition);
854
+ z-index: 1000;
855
+ }
856
+
857
+ .theme-toggle:hover {
858
+ background: var(--accent-color);
859
+ color: white;
860
+ box-shadow: var(--shadow-lg);
861
+ }
862
+
863
+ .theme-toggle::before {
864
+ content: '🌙';
865
+ font-size: 1rem;
866
+ }
867
+
868
+ /* Dark mode styles */
869
+ [data-theme="dark"] {
870
+ --text-color: #f9fafb;
871
+ --text-light: #d1d5db;
872
+ --text-muted: #9ca3af;
873
+ --bg-color: #111827;
874
+ --card-bg: #1f2937;
875
+ --border-color: #374151;
876
+ --border-light: #4b5563;
877
+ }
878
+
879
+ [data-theme="dark"] body {
880
+ background: var(--bg-color);
881
+ }
882
+
883
+ [data-theme="dark"] .theme-toggle::before {
884
+ content: '☀️';
885
+ }
886
+
887
+ /* Professional scrollbar */
888
+ ::-webkit-scrollbar {
889
+ width: 8px;
890
+ }
891
+
892
+ ::-webkit-scrollbar-track {
893
+ background: var(--border-light);
894
+ }
895
+
896
+ ::-webkit-scrollbar-thumb {
897
+ background: var(--accent-color);
898
+ border-radius: 4px;
899
+ }
900
+
901
+ ::-webkit-scrollbar-thumb:hover {
902
+ background: var(--primary-color);
903
+ }
904
+
905
+ /* Professional selection styling */
906
+ ::selection {
907
+ background: var(--accent-color);
908
+ color: white;
909
+ }
910
+
911
+ ::-moz-selection {
912
+ background: var(--accent-color);
913
+ color: white;
914
+ }
915
+
916
+ /* Enhanced focus states for accessibility */
917
+ .theme-toggle:focus {
918
+ outline: 2px solid var(--accent-color);
919
+ outline-offset: 2px;
920
+ }
921
+
922
+ .footer a:focus {
923
+ outline: 2px solid var(--accent-color);
924
+ outline-offset: 2px;
925
+ border-radius: 4px;
926
+ }
927
+
928
+ /* Print styles */
929
+ @media print {
930
+ body {
931
+ background: white !important;
932
+ color: black !important;
933
+ }
934
+
935
+ .header {
936
+ background: #f8f9fa !important;
937
+ color: black !important;
938
+ box-shadow: none !important;
939
+ }
940
+
941
+ .content {
942
+ box-shadow: none !important;
943
+ }
944
+
945
+ .theme-toggle {
946
+ display: none !important;
947
+ }
948
+
949
+ .footer {
950
+ background: #f8f9fa !important;
951
+ border-top: 1px solid #ddd !important;
952
+ }
953
+ }
954
+
955
+ @media (max-width: 768px) {
956
+ .header {
957
+ padding: 1.5rem 2rem;
958
+ }
959
+
960
+ .header-content {
961
+ flex-direction: column;
962
+ align-items: flex-start;
963
+ gap: 1rem;
964
+ }
965
+
966
+ .header h1 {
967
+ font-size: 2rem;
968
+ }
969
+
970
+ .content {
971
+ padding: 2rem;
972
+ }
973
+
974
+ .markdown-table {
975
+ font-size: 0.875rem;
976
+ }
977
+
978
+ .markdown-table th,
979
+ .markdown-table td {
980
+ padding: 0.5rem;
981
+ }
982
+
983
+ .theme-toggle {
984
+ top: 1rem;
985
+ right: 1rem;
986
+ width: 40px;
987
+ height: 40px;
988
+ }
989
+
990
+ .footer {
991
+ padding: 1.5rem 2rem;
992
+ }
993
+
994
+ .footer-content {
995
+ flex-direction: column;
996
+ gap: 1rem;
997
+ text-align: center;
998
+ }
999
+ }
1000
+
1001
+ @media (max-width: 480px) {
1002
+ .header {
1003
+ padding: 1rem;
1004
+ }
1005
+
1006
+ .header h1 {
1007
+ font-size: 1.75rem;
1008
+ }
1009
+
1010
+ .content {
1011
+ padding: 1.5rem;
1012
+ }
1013
+
1014
+ .markdown-table {
1015
+ font-size: 0.8rem;
1016
+ }
1017
+
1018
+ .markdown-table th,
1019
+ .markdown-table td {
1020
+ padding: 0.375rem;
1021
+ }
1022
+ }
1023
+ """
1024
+
1025
+
1026
+ def _get_table_css_styles() -> str:
1027
+ """Get additional CSS styles for tables."""
1028
+ return """
1029
+ .data-section {
1030
+ margin-bottom: 4rem;
1031
+ position: relative;
1032
+ }
1033
+
1034
+ .data-section::before {
1035
+ content: '';
1036
+ position: absolute;
1037
+ top: -1rem;
1038
+ left: 0;
1039
+ right: 0;
1040
+ height: 2px;
1041
+ background: linear-gradient(90deg, var(--primary-color), var(--secondary-color), var(--accent-color));
1042
+ border-radius: 1px;
1043
+ }
1044
+
1045
+ .section-title {
1046
+ color: var(--text-color);
1047
+ font-size: 1.8rem;
1048
+ margin-bottom: 2rem;
1049
+ padding-bottom: 1rem;
1050
+ border-bottom: 3px solid var(--primary-color);
1051
+ position: relative;
1052
+ font-weight: 700;
1053
+ }
1054
+
1055
+ .section-title::after {
1056
+ content: '';
1057
+ position: absolute;
1058
+ bottom: -3px;
1059
+ left: 0;
1060
+ width: 50px;
1061
+ height: 3px;
1062
+ background: var(--accent-color);
1063
+ }
1064
+
1065
+ .table-container {
1066
+ overflow-x: auto;
1067
+ margin: 2rem 0;
1068
+ border-radius: var(--border-radius);
1069
+ box-shadow: var(--shadow-lg);
1070
+ position: relative;
1071
+ background: var(--card-bg);
1072
+ }
1073
+
1074
+ .table-container::before {
1075
+ content: '';
1076
+ position: absolute;
1077
+ top: 0;
1078
+ left: 0;
1079
+ right: 0;
1080
+ height: 4px;
1081
+ background: linear-gradient(90deg, var(--primary-color), var(--secondary-color), var(--accent-color));
1082
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
1083
+ }
1084
+
1085
+ .data-table {
1086
+ width: 100%;
1087
+ border-collapse: collapse;
1088
+ background: var(--card-bg);
1089
+ font-size: 1rem;
1090
+ position: relative;
1091
+ }
1092
+
1093
+ .data-table th {
1094
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
1095
+ color: white;
1096
+ padding: 1.5rem 1.5rem;
1097
+ text-align: left;
1098
+ font-weight: 700;
1099
+ font-size: 0.95rem;
1100
+ text-transform: uppercase;
1101
+ letter-spacing: 0.8px;
1102
+ position: relative;
1103
+ }
1104
+
1105
+ .data-table th::after {
1106
+ content: '';
1107
+ position: absolute;
1108
+ bottom: 0;
1109
+ left: 0;
1110
+ right: 0;
1111
+ height: 2px;
1112
+ background: linear-gradient(90deg, transparent, white, transparent);
1113
+ }
1114
+
1115
+ .data-table th:first-child {
1116
+ border-top-left-radius: var(--border-radius);
1117
+ }
1118
+
1119
+ .data-table th:last-child {
1120
+ border-top-right-radius: var(--border-radius);
1121
+ }
1122
+
1123
+ .data-table td {
1124
+ padding: 1.25rem 1.5rem;
1125
+ border-bottom: 1px solid var(--border-color);
1126
+ vertical-align: top;
1127
+ font-size: 1rem;
1128
+ line-height: 1.6;
1129
+ }
1130
+
1131
+ .data-table tr:nth-child(even) {
1132
+ background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
1133
+ }
1134
+
1135
+ .data-table tr:hover {
1136
+ background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
1137
+ transform: scale(1.005);
1138
+ transition: var(--transition);
1139
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
1140
+ }
1141
+
1142
+ .data-table tr:last-child td:first-child {
1143
+ border-bottom-left-radius: var(--border-radius);
1144
+ }
1145
+
1146
+ .data-table tr:last-child td:last-child {
1147
+ border-bottom-right-radius: var(--border-radius);
1148
+ }
1149
+
1150
+ .data-table tr:last-child td {
1151
+ border-bottom: none;
1152
+ }
1153
+
1154
+ .no-data {
1155
+ text-align: center;
1156
+ color: var(--text-light);
1157
+ font-style: italic;
1158
+ padding: 3rem;
1159
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
1160
+ border-radius: var(--border-radius);
1161
+ font-size: 1.1rem;
1162
+ position: relative;
1163
+ }
1164
+
1165
+ .no-data::before {
1166
+ content: '📊';
1167
+ font-size: 2rem;
1168
+ display: block;
1169
+ margin-bottom: 1rem;
1170
+ opacity: 0.5;
1171
+ }
1172
+
1173
+ /* Enhanced table animations */
1174
+ .data-table tr {
1175
+ transition: var(--transition);
1176
+ }
1177
+
1178
+ .data-table td {
1179
+ transition: var(--transition);
1180
+ }
1181
+
1182
+ .data-table tr:hover td {
1183
+ color: var(--text-color);
1184
+ font-weight: 500;
1185
+ }
1186
+
1187
+ /* Table scrollbar styling */
1188
+ .table-container::-webkit-scrollbar {
1189
+ height: 8px;
1190
+ }
1191
+
1192
+ .table-container::-webkit-scrollbar-track {
1193
+ background: var(--border-color);
1194
+ border-radius: 4px;
1195
+ }
1196
+
1197
+ .table-container::-webkit-scrollbar-thumb {
1198
+ background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
1199
+ border-radius: 4px;
1200
+ }
1201
+
1202
+ .table-container::-webkit-scrollbar-thumb:hover {
1203
+ background: linear-gradient(90deg, var(--secondary-color), var(--accent-color));
1204
+ }
1205
+
1206
+ @media (max-width: 768px) {
1207
+ .data-table {
1208
+ font-size: 0.9rem;
1209
+ }
1210
+
1211
+ .data-table th,
1212
+ .data-table td {
1213
+ padding: 1rem;
1214
+ }
1215
+
1216
+ .section-title {
1217
+ font-size: 1.5rem;
1218
+ }
1219
+ }
1220
+
1221
+ @media (max-width: 480px) {
1222
+ .data-table {
1223
+ font-size: 0.8rem;
1224
+ }
1225
+
1226
+ .data-table th,
1227
+ .data-table td {
1228
+ padding: 0.75rem;
1229
+ }
1230
+
1231
+ .section-title {
1232
+ font-size: 1.3rem;
1233
+ }
1234
+ }
1235
+ """