| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784 |
- import os
- import glob
- import re
- import fitz
- import shutil
- import subprocess
- from datetime import datetime
- from markdown_pdf import MarkdownPdf, Section
- # List of key technical terms to search and index across all documents
- KEY_TERMS = [
- "AI", "MySQL", "Zabbix", "Docker", "Streamlit", "Nginx", "Airflow",
- "RAG", "Allergens", "Vitamins", "Minerals", "Clinical", "WSL", "Ollama",
- "LLM", "Database", "Security", "Telemetry", "Backup", "Firewall", "SMTP"
- ]
- def clean_header_text(text):
- # Remove markdown links: [text](url) -> text
- text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
- # Remove bold/italic/code formatting
- text = re.sub(r'[*_`]', '', text)
- return text.strip()
- def extract_headers(md_content):
- headers = []
- first_h1 = True
- for line in md_content.splitlines():
- if line.startswith('#'):
- match = re.match(r'^(#{1,3})\s+(.+)$', line)
- if match:
- level = len(match.group(1))
- t = match.group(2).strip()
- # Skip the document main title (first H1)
- if level == 1 and first_h1:
- first_h1 = False
- continue
- headers.append((level, t))
- return headers
- def generate_references(md_content):
- links = re.findall(r'\[([^\]]+)\]\((https?://[^\)]+)\)', md_content)
- lines = ["\n\n## References\n"]
- if links:
- seen = set()
- count = 1
- for text, url in links:
- if url not in seen:
- seen.add(url)
- clean_text = re.sub(r'[*_`]', '', text)
- lines.append(f"{count}. **{clean_text}**: {url}")
- count += 1
- if count > 10:
- break
- else:
- lines.append("1. **OpenFoodFacts Dataset & API Catalog**: Detailed food ingredients database. (https://world.openfoodfacts.org/)")
- lines.append("2. **Ollama Local LLM Inference Engine**: Lightweight instruction-following llama3.2 runtimes. (https://ollama.com/)")
- lines.append("3. **Zabbix Enterprise Telemetry and Monitoring**: System health and performance logging. (https://www.zabbix.com/)")
- return "\n".join(lines)
- def sanitize_name(name):
- if not name:
- return "Francois Lange"
- name_lower = name.lower()
- if "fran" in name_lower or "lange" in name_lower or "lanfr" in name_lower:
- return "Francois Lange"
- return name
- def get_git_info_for_file(file_path):
- try:
- cmd = [
- "git", "log", "-1",
- "--date=format:%Y/%m/%d %H:%M:%S",
- "--format=%an|%ae|%ad|%cn|%ce|%cd|%H|%D|%N",
- "--", file_path
- ]
- out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip()
- if out:
- parts = out.split('|')
- if len(parts) == 9:
- parts[0] = sanitize_name(parts[0])
- parts[3] = sanitize_name(parts[3])
- return parts
- except Exception:
- pass
- author_name = "Francois Lange"
- try:
- author_email = subprocess.check_output(["git", "config", "user.email"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "lanfr144@school.lu"
- except Exception:
- author_email = "lanfr144@school.lu"
- now_str = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
- return [author_name, author_email, now_str, author_name, author_email, now_str, "Not Committed Yet", "local", "none"]
- def compile_pdf_document(md_file, title, md_content, toc_content, index_content, references_content, root_dir, user_css):
- pdf = MarkdownPdf(optimize=True, plugins={"mermaid": {}})
- base_name = os.path.basename(md_file)
-
- # 1. Section 1: Cover Page
- cover_html = f"""
- <div style="text-align: center; font-family: 'Helvetica', 'Arial', sans-serif; padding-top: 150px; padding-bottom: 150px;">
- <h1 style="font-size: 28pt; font-weight: bold; margin-bottom: 20px; color: #000000; line-height: 1.2;">{title}</h1>
- <p style="font-size: 14pt; color: #666666; margin-top: 10px; margin-bottom: 150px; letter-spacing: 1px; text-transform: uppercase;">Project Deliverable</p>
- <div style="border-top: 2px solid #eaeaea; width: 80%; margin: 0 auto 50px auto;"></div>
- <table style="margin: 0 auto; text-align: left; font-size: 12pt; border: none; width: 60%; line-height: 2.0; border-collapse: collapse;">
- <tr style="border: none;"><td style="font-weight: bold; color: #555555; padding: 5px; border: none; width: 40%;">Course Class:</td><td style="color: #111111; padding: 5px; border: none;">B1AI</td></tr>
- <tr style="border: none;"><td style="font-weight: bold; color: #555555; padding: 5px; border: none;">Author:</td><td style="color: #111111; padding: 5px; border: none;">François LANGE</td></tr>
- <tr style="border: none;"><td style="font-weight: bold; color: #555555; padding: 5px; border: none;">Date:</td><td style="color: #111111; padding: 5px; border: none;">2026/06/19</td></tr>
- </table>
- </div>
- """
- pdf.add_section(Section(cover_html, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css)
-
- # 2. Section 2: Blank Page
- blank_content = "<div style='height: 1000px;'> </div>"
- pdf.add_section(Section(blank_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css)
-
- # 3. Section 3: Printed Table of Contents
- pdf.add_section(Section(toc_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css)
-
- # 4. Section 4: Main Content (with potential landscape splitting for project_report)
- if base_name == 'project_report.md':
- parts = md_content.split('## 2. Project File Catalog & Documentation')
- if len(parts) == 2:
- portrait_part1 = parts[0]
- remaining = parts[1]
- parts2 = remaining.split('## 3. Directory Structure Map')
- if len(parts2) == 2:
- landscape_part = '## 2. Project File Catalog & Documentation' + parts2[0]
- portrait_part2 = '## 3. Directory Structure Map' + parts2[1]
-
- pdf.add_section(Section(portrait_part1, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css)
- pdf.add_section(Section(landscape_part, paper_size="A4-L", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css)
- pdf.add_section(Section(portrait_part2 + "\n" + references_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css)
- else:
- pdf.add_section(Section(md_content + "\n" + references_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css)
- else:
- pdf.add_section(Section(md_content + "\n" + references_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css)
- else:
- pdf.add_section(Section(md_content + "\n" + references_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css)
-
- # 5. Section 5: Printed Index
- pdf.add_section(Section(index_content, paper_size="A4", root=root_dir, borders=(36, 60, -36, -36)), user_css=user_css)
-
- return pdf
- def generate_html_portal(root_dir, md_files):
- portal_path = os.path.join(root_dir, 'index.html')
- doc_metadata = {
- "technical_document": "Core technical specification details of the local clinical Food AI including system architecture and data models.",
- "distributed_deployment": "Installation topology guide for deploying the application stack across multiple hypervisors (WSL, Hyper-V, VirtualBox) or physical servers.",
- "disaster_recovery_plan": "Recovery policies, backup strategies, and restore drills to ensure zero data loss during infrastructure outages.",
- "Uninstall_Guide": "Clean uninstallation workflow to remove all database, cache, airflow, and container resources from physical hosts.",
- "Operator_Installation_Guide": "Naked OS installation guide covering package installations, python dependencies, and native system configs.",
- "project_report": "Comprehensive sprint report detailing system catalog, directory mappings, and final scrum delivery notes.",
- "retro_planning": "Timeline planning showing daily milestones, task allocations, and sprint scheduling sheets.",
- "User_Guide": "User manual outlining Streamlit login, meal planning, plate building, and chat assistant instructions.",
- "zabbix_monitoring": "SNMP trap configurations, alert channels, and Zabbix server integration mappings.",
- "Global_Index": "Unified global term index cross-referencing keywords across all documentation files."
- }
-
- cards_html = []
- for md_file in sorted(md_files):
- base = os.path.basename(md_file).replace('.md', '')
- if "Header_Footer" in base:
- continue
-
- pdf_rel_path = f"docs/{base}.pdf"
- md_rel_path = f"docs/{base}.md"
- title = base.replace('_', ' ').replace('-', ' ').title()
-
- if base == "project_report":
- pdf_rel_path = "Project.pdf"
- title = "Project Report"
- elif base == "retro_planning":
- pdf_rel_path = "Retro Planning.pdf"
- title = "Retro Planning"
-
- desc = doc_metadata.get(base, "Project documentation deliverable covering specifications and guides.")
-
- cards_html.append(f"""
- <div class="card">
- <div class="card-body">
- <div>
- <h3 class="card-title">{title}</h3>
- <p class="card-text">{desc}</p>
- </div>
- <div class="card-links">
- <a href="{pdf_rel_path}" class="btn btn-primary" target="_blank">📄 View PDF</a>
- <a href="{md_rel_path}" class="btn btn-secondary" target="_blank">📝 View Markdown</a>
- </div>
- </div>
- </div>
- """)
-
- cards_str = "\n".join(cards_html)
-
- html_content = f"""<!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Local Food AI Documentation Portal</title>
- <style>
- body {{
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background-color: #f4f7f6;
- margin: 0;
- padding: 0;
- color: #333;
- }}
- .header {{
- background: linear-gradient(135deg, #1b263b, #0d1b2a);
- color: white;
- padding: 40px 20px;
- text-align: center;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
- }}
- .header h1 {{
- margin: 0;
- font-size: 28pt;
- font-weight: 600;
- }}
- .header p {{
- margin: 10px 0 0 0;
- font-size: 14pt;
- color: #a3b18a;
- }}
- .container {{
- max-width: 1200px;
- margin: 40px auto;
- padding: 0 20px;
- }}
- .class-meta {{
- background-color: white;
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 30px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
- display: flex;
- justify-content: space-around;
- flex-wrap: wrap;
- font-size: 11pt;
- }}
- .class-meta div {{
- margin: 10px;
- }}
- .grid {{
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
- gap: 25px;
- }}
- .card {{
- background-color: white;
- border-radius: 8px;
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
- transition: transform 0.2s ease, box-shadow 0.2s ease;
- overflow: hidden;
- border: 1px solid #e9ecef;
- }}
- .card:hover {{
- transform: translateY(-5px);
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
- }}
- .card-body {{
- padding: 25px;
- display: flex;
- flex-direction: column;
- height: 180px;
- justify-content: space-between;
- }}
- .card-title {{
- margin: 0 0 10px 0;
- font-size: 15pt;
- color: #1b263b;
- }}
- .card-text {{
- font-size: 10pt;
- color: #666;
- margin: 0 0 20px 0;
- line-height: 1.5;
- }}
- .card-links {{
- display: flex;
- gap: 10px;
- }}
- .btn {{
- display: inline-block;
- padding: 10px 15px;
- font-size: 9.5pt;
- text-decoration: none;
- border-radius: 5px;
- text-align: center;
- font-weight: 600;
- flex: 1;
- }}
- .btn-primary {{
- background-color: #0077b6;
- color: white;
- }}
- .btn-primary:hover {{
- background-color: #0096c7;
- }}
- .btn-secondary {{
- background-color: #eaeef1;
- color: #495057;
- }}
- .btn-secondary:hover {{
- background-color: #dee2e6;
- }}
- .footer {{
- text-align: center;
- padding: 40px;
- font-size: 10pt;
- color: #888;
- }}
- </style>
- </head>
- <body>
- <div class="header">
- <h1>Local Food AI</h1>
- <p>Unified Project Documentation Portal</p>
- </div>
- <div class="container">
- <div class="class-meta">
- <div><strong>Class Course:</strong> B1AI</div>
- <div><strong>Student Author:</strong> François LANGE</div>
- <div><strong>Academic Year:</strong> 2025/2026</div>
- </div>
- <div class="grid">
- {cards_str}
- </div>
- </div>
- <div class="footer">
- © 2026 François LANGE (Class B1AI). All rights reserved.
- </div>
- </body>
- </html>
- """
- with open(portal_path, 'w', encoding='utf-8') as f:
- f.write(html_content)
- print("Created index.html portal successfully.")
- def main():
- docs_dir = os.path.join(os.path.dirname(__file__), '..', 'docs')
- md_files = glob.glob(os.path.join(docs_dir, '*.md'))
-
- # Exclude Header/Footer files and previously created Global_Index if any
- md_files = [f for f in md_files if "Header_Footer" not in os.path.basename(f) and "Global_Index" not in os.path.basename(f)]
-
- if not md_files:
- print("No markdown files found in docs/")
- return
-
- root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')).replace('\\', '/')
-
- user_css = """
- * {
- color: #1a1a1a !important;
- }
- body {
- font-family: 'Helvetica', 'Arial', sans-serif !important;
- color: #1a1a1a !important;
- background-color: #ffffff !important;
- }
- h1, h2, h3, h4, h5, h6, h1 *, h2 *, h3 *, h4 *, h5 *, h6 * {
- color: #000000 !important;
- margin-top: 18px !important;
- margin-bottom: 8px !important;
- }
- pre {
- background-color: #f8f9fa !important;
- border: 1px solid #e9ecef !important;
- padding: 2px !important;
- margin-top: 8px !important;
- margin-bottom: 16px !important;
- border-radius: 3px !important;
- font-family: 'Courier New', 'Courier', monospace !important;
- font-size: 10pt !important;
- white-space: pre-wrap !important;
- word-break: break-all !important;
- }
- pre code, pre * {
- font-family: 'Courier New', 'Courier', monospace !important;
- font-size: 10pt !important;
- color: #212529 !important;
- background-color: #f8f9fa !important;
- white-space: pre-wrap !important;
- word-break: break-all !important;
- }
- code {
- font-family: 'Courier New', 'Courier', monospace !important;
- font-size: 10pt !important;
- color: #b02a37 !important;
- background-color: #f8f9fa !important;
- padding: 2px 4px !important;
- border-radius: 3px !important;
- white-space: pre-wrap !important;
- word-break: break-all !important;
- }
- a, a * {
- color: #0d6efd !important;
- }
- blockquote, blockquote * {
- color: #555555 !important;
- border-left: 4px solid #ccc !important;
- padding-left: 10px !important;
- }
- table, tr, td, th, table * {
- color: #1a1a1a !important;
- border-color: #cccccc !important;
- }
- th {
- background-color: #f2f2f2 !important;
- }
- ul, li {
- list-style-type: disc !important;
- }
- """
- global_term_index = {} # Structure: { term: [ (doc_title, pdf_path, [page_numbers]) ] }
- doc_data = {} # Structure: { md_file: { title, md_content, headers, references } }
- # ==========================================
- # PASS 1: Generate Draft PDFs and Find Pages
- # ==========================================
- print("=== STARTING DRAFT COMPILATION AND ANALYSIS PASS ===")
-
- for md_file in md_files:
- print(f"Analyzing {os.path.basename(md_file)}...")
-
- with open(md_file, 'r', encoding='utf-8') as f:
- md_content = f.read()
-
- # Format replacements
- base_name = os.path.basename(md_file)
- info = get_git_info_for_file(md_file)
- replacement = f"$Format:LocalFoodAI_lanfr144:generate_pdfs.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
- pattern = r'\$Format:LocalFoodAI_lanfr144:generate_pdfs.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$'
- md_content = re.sub(pattern, replacement, md_content)
-
- md_content = re.sub(r'file:///.*?/docs/([a-zA-Z0-9_-]+)\.md', r'\1.pdf', md_content, flags=re.IGNORECASE)
- md_content = re.sub(r'file:///.*?/Food/([a-zA-Z0-9_.-]+)', r'../\1', md_content, flags=re.IGNORECASE)
- md_content = re.sub(r'(```[a-zA-Z0-9_-]*\n[\s\S]*?\n```)', r'\1\n\n', md_content)
- md_content = re.sub(r'\]\((?!http|https)([a-zA-Z0-9_./-]+)\.md(#[a-zA-Z0-9_-]+)?\)', r'](\1.pdf\2)', md_content, flags=re.IGNORECASE)
- # Title parsing
- title_match = re.search(r'^#\s+(.+)$', md_content, re.MULTILINE)
- if title_match:
- doc_title = title_match.group(1).strip()
- doc_title = re.sub(r'[*_`#]', '', doc_title).strip()
- else:
- doc_title = base_name.replace('.md', '').replace('_', ' ').title()
- headers = extract_headers(md_content)
- references = generate_references(md_content)
-
- # Build placeholders for TOC and Index in the draft
- toc_placeholder = "# Table of Contents\n\n"
- for level, t in headers:
- indent = " " * (level - 1)
- toc_placeholder += f"{indent}- {t} .................................................... Page 00\n"
-
- index_placeholder = "## Index\n\n"
- for term in KEY_TERMS:
- index_placeholder += f"- **{term}**: Page 00\n"
- doc_data[md_file] = {
- "title": doc_title,
- "md_content": md_content,
- "headers": headers,
- "references": references
- }
- try:
- # Compile draft PDF
- pdf = compile_pdf_document(
- md_file, doc_title, md_content, toc_placeholder,
- index_placeholder, references, root_dir, user_css
- )
- draft_pdf_file = md_file.replace('.md', '_draft.pdf')
- pdf.save(draft_pdf_file)
- pdf.temp_files.clean()
-
- # Analyze draft PDF using fitz
- doc = fitz.open(draft_pdf_file)
-
- # 1. Map headers to printed page numbers
- page_map = {}
- for level, raw_t in headers:
- clean_t = clean_header_text(raw_t)
- found_page = None
- for page_idx in range(3, len(doc)): # Main content starts on page index 3 (Page 2)
- page = doc[page_idx]
- rects = page.search_for(clean_t)
- if rects:
- found_page = page_idx - 1 # printed Page 1 is index 2 (TOC page)
- break
- if found_page is None:
- found_page = 2 # default fallback
- page_map[raw_t] = found_page
-
- # 2. Map key terms to printed page numbers
- term_pages = {}
- for term in KEY_TERMS:
- matches = []
- for page_idx in range(2, len(doc)): # TOC page (index 2) onwards
- page = doc[page_idx]
- rects = page.search_for(term)
- if rects:
- matches.append(page_idx - 1)
- if matches:
- term_pages[term] = matches
- # Populate global index
- if term not in global_term_index:
- global_term_index[term] = []
- global_term_index[term].append((doc_title, md_file.replace('.md', '.pdf'), matches))
-
- doc_data[md_file]["page_map"] = page_map
- doc_data[md_file]["term_pages"] = term_pages
-
- doc.close()
- # Clean up draft PDF
- if os.path.exists(draft_pdf_file):
- os.remove(draft_pdf_file)
-
- except Exception as err:
- print(f"Draft analysis failed for {os.path.basename(md_file)}: {err}")
- # ==========================================
- # PASS 2: Create Global Index and Compile Finals
- # ==========================================
- print("\n=== COMPILING GLOBAL INDEX DOCUMENT ===")
- global_index_md_path = os.path.join(docs_dir, 'Global_Index.md')
- global_index_pdf_path = os.path.join(docs_dir, 'Global_Index.pdf')
-
- # Generate Global_Index.md content
- global_lines = [
- "# Global Project Index\n",
- "This document provides a unified cross-reference index of all key technical terms and concepts across the LocalFoodAI documentation suite. Click on any document link to open it at the specific page in your viewer.\n",
- "## Index of Terms\n"
- ]
- for term in sorted(global_term_index.keys()):
- occurrences = global_term_index[term]
- global_lines.append(f"### {term}\n")
- for doc_title, pdf_path, pages in occurrences:
- pages_str = ", ".join([f"Page {p}" for p in pages])
- pdf_rel = os.path.basename(pdf_path)
- first_page = pages[0]
- # Format link to open PDF on exact page index
- global_lines.append(f"- **{doc_title}**: {pages_str} | [Open PDF at Page {first_page}]({pdf_rel}#page={first_page + 1})")
- global_lines.append("")
-
- with open(global_index_md_path, 'w', encoding='utf-8') as f:
- f.write("\n".join(global_lines))
- print("Saved docs/Global_Index.md")
- # Add Global_Index.md to the compilation files list
- md_files.append(global_index_md_path)
-
- # Generate draft analysis for Global_Index.md as well
- with open(global_index_md_path, 'r', encoding='utf-8') as f:
- global_md_content = f.read()
-
- global_headers = extract_headers(global_md_content)
- global_references = generate_references(global_md_content)
-
- toc_placeholder = "# Table of Contents\n\n"
- for level, t in global_headers:
- indent = " " * (level - 1)
- toc_placeholder += f"{indent}- {t} .................................................... Page 00\n"
-
- index_placeholder = "## Index\n\n"
- for term in KEY_TERMS:
- index_placeholder += f"- **{term}**: Page 00\n"
-
- doc_data[global_index_md_path] = {
- "title": "Global Project Index",
- "md_content": global_md_content,
- "headers": global_headers,
- "references": global_references
- }
-
- # Run a quick draft pass for Global Index itself to find header and index pages
- try:
- pdf = compile_pdf_document(
- global_index_md_path, "Global Project Index", global_md_content,
- toc_placeholder, index_placeholder, global_references, root_dir, user_css
- )
- draft_global_pdf = global_index_pdf_path.replace('.pdf', '_draft.pdf')
- pdf.save(draft_global_pdf)
- pdf.temp_files.clean()
-
- doc = fitz.open(draft_global_pdf)
- global_page_map = {}
- for level, raw_t in global_headers:
- clean_t = clean_header_text(raw_t)
- found_page = None
- for page_idx in range(3, len(doc)):
- page = doc[page_idx]
- rects = page.search_for(clean_t)
- if rects:
- found_page = page_idx - 1
- break
- if found_page is None:
- found_page = 2
- global_page_map[raw_t] = found_page
-
- global_term_pages = {}
- for term in KEY_TERMS:
- matches = []
- for page_idx in range(2, len(doc)):
- page = doc[page_idx]
- rects = page.search_for(term)
- if rects:
- matches.append(page_idx - 1)
- if matches:
- global_term_pages[term] = matches
-
- doc_data[global_index_md_path]["page_map"] = global_page_map
- doc_data[global_index_md_path]["term_pages"] = global_term_pages
- doc.close()
- if os.path.exists(draft_global_pdf):
- os.remove(draft_global_pdf)
- except Exception as err:
- print(f"Draft analysis failed for Global_Index.md: {err}")
- # ==========================================
- # PASS 3: Generate Final PDFs
- # ==========================================
- print("\n=== COMPILING FINAL GENERATED PDF FILES ===")
-
- for md_file in md_files:
- pdf_file = md_file.replace('.md', '.pdf')
- base_name = os.path.basename(md_file)
-
- data = doc_data.get(md_file)
- if not data:
- continue
-
- title = data["title"]
- md_content = data["md_content"]
- headers = data["headers"]
- references = data["references"]
- page_map = data.get("page_map", {})
- term_pages = data.get("term_pages", {})
- # 1. Construct final printed TOC
- toc_lines = ["# Table of Contents\n\n"]
- for level, t in headers:
- indent = " " * (level - 1)
- p_num = page_map.get(t, 2)
- toc_lines.append(f"{indent}- {t} .................................................... Page {p_num}")
- toc_content = "\n".join(toc_lines)
- # 2. Construct final printed Index
- index_lines = ["## Index\n\n"]
- has_index_terms = False
- for term in KEY_TERMS:
- if term in term_pages:
- has_index_terms = True
- pages_str = ", ".join([f"Page {p}" for p in term_pages[term]])
- index_lines.append(f"- **{term}**: {pages_str}")
- if not has_index_terms:
- index_lines.append("- No key terms found in this document.")
- index_content = "\n".join(index_lines)
- try:
- pdf = compile_pdf_document(
- md_file, title, md_content, toc_content,
- index_content, references, root_dir, user_css
- )
-
- doc = pdf._make_doc()
- total_pages = len(doc)
-
- am_path = os.path.join(docs_dir, 'am.png')
- bts_path = os.path.join(docs_dir, 'Bts.png')
-
- width_am, height_am = 146.5, 24.5
- if os.path.exists(am_path):
- try:
- pix_am = fitz.Pixmap(am_path)
- width_am = pix_am.width * 0.1
- height_am = pix_am.height * 0.1
- except Exception: pass
-
- width_bts, height_bts = 67.1, 36.8
- if os.path.exists(bts_path):
- try:
- pix_bts = fitz.Pixmap(bts_path)
- width_bts = pix_bts.width * 0.1
- height_bts = pix_bts.height * 0.1
- except Exception: pass
-
- for page_idx in range(total_pages):
- # COVER & BLANK page should not contain headers, footers, or logos
- if page_idx in [0, 1]:
- continue
-
- page = doc[page_idx]
- width = page.rect.width
- height = page.rect.height
-
- # Header layout
- if os.path.exists(am_path):
- image_rect = fitz.Rect(48, 12, 48 + width_am, 12 + height_am)
- page.insert_image(image_rect, filename=am_path, overlay=False)
-
- if os.path.exists(bts_path):
- bts_rect = fitz.Rect(width - 48 - width_bts, 8, width - 48, 8 + height_bts)
- page.insert_image(bts_rect, filename=bts_path, overlay=False)
-
- # Title text
- fontsize = 8
- fontname = "helv"
- m_width = fitz.get_text_length(title, fontname=fontname, fontsize=fontsize)
- space_start = 48 + width_am
- space_end = width - 48 - width_bts
- title_x = space_start + (space_end - space_start - m_width) / 2
-
- page.insert_text(
- fitz.Point(title_x, 26),
- title,
- fontname=fontname,
- fontsize=fontsize,
- color=(0.5, 0.5, 0.5)
- )
-
- # Footer layout (Page numbers start counting at TOC as Page 1)
- l_footer = "lanfr144"
- footer_text = f"Page {page_idx - 1} of {total_pages - 2}"
- f_width = fitz.get_text_length(footer_text, fontname=fontname, fontsize=fontsize)
-
- page.insert_text(
- fitz.Point(48, height - 22),
- l_footer,
- fontname=fontname,
- fontsize=fontsize,
- color=(0.5, 0.5, 0.5)
- )
-
- page.insert_text(
- fitz.Point((width - f_width) / 2, height - 22),
- footer_text,
- fontname=fontname,
- fontsize=fontsize,
- color=(0.5, 0.5, 0.5)
- )
-
- r_footer = "DOPRO1"
- r_footer_width = fitz.get_text_length(r_footer, fontname=fontname, fontsize=fontsize)
- page.insert_text(
- fitz.Point(width - 48 - r_footer_width, height - 22),
- r_footer,
- fontname=fontname,
- fontsize=fontsize,
- color=(0.5, 0.5, 0.5)
- )
-
- doc.save(pdf_file, clean=True, garbage=3, deflate=True)
- doc.close()
- pdf.temp_files.clean()
-
- print(f"Saved {os.path.basename(pdf_file)}")
-
- # Copy to workspace root if applicable
- if base_name == 'project_report.md':
- dest = os.path.join(root_dir, 'Project.pdf')
- if os.path.exists(dest): os.remove(dest)
- shutil.copy2(pdf_file, dest)
- print("Successfully updated root Project.pdf")
- elif base_name == 'retro_planning.md':
- dest = os.path.join(root_dir, 'Retro Planning.pdf')
- if os.path.exists(dest): os.remove(dest)
- shutil.copy2(pdf_file, dest)
- print("Successfully updated root Retro Planning.pdf")
-
- except Exception as e:
- print(f"WARNING: Could not save {os.path.basename(pdf_file)}. File might be locked or open in a viewer. Error: {e}")
- # Generate HTML catalog portal in the workspace root
- generate_html_portal(root_dir, md_files)
- if __name__ == "__main__":
- main()
|