diff --git a/.gemini/assets/itguys_logo_footer.png b/.gemini/assets/itguys_logo_footer.png index 439318d..3798fd9 100644 Binary files a/.gemini/assets/itguys_logo_footer.png and b/.gemini/assets/itguys_logo_footer.png differ diff --git a/.gemini/convert_to_pdf.py b/.gemini/convert_to_pdf.py index 72aab82..b32b458 100644 --- a/.gemini/convert_to_pdf.py +++ b/.gemini/convert_to_pdf.py @@ -2,7 +2,6 @@ import sys import os import re from datetime import datetime -import argparse # Dependency check try: @@ -19,17 +18,34 @@ except ImportError: # Assets BASE_DIR = os.path.dirname(os.path.abspath(__file__)) LOGO_PATH = os.path.join(BASE_DIR, "assets", "itguys_logo_main.png") +LOGO_FOOTER_PATH = os.path.join(BASE_DIR, "assets", "itguys_logo_footer.png") # Colors (Premium Palette) -COLOR_PRIMARY = (20, 120, 207) # #1478cf -COLOR_TEXT_MAIN = (50, 60, 70) # #323C46 +COLOR_PRIMARY = (20, 120, 207) # #1478cf (Blue) +COLOR_SECONDARY = (0, 247, 255) # #00f7ff (Cyan) +COLOR_ACCENT = (46, 204, 113) # #2ecc71 (Green) +COLOR_TEXT_MAIN = (50, 60, 70) # Dark Grey (Body) COLOR_BG_LIGHT = (250, 250, 252) -COLOR_INFO_BG = (235, 245, 255) + +# Specific Header/Section Colors +COLOR_HEADER_BG = (20, 120, 207) # #1478cf (Blue) +COLOR_SECTION_BG = (235, 242, 250) # Light Blue +COLOR_SECTION_TEXT = (20, 80, 140) # Dark Blue + +# Terminal Code Block Colors +COLOR_CODE_BG = (30, 30, 30) # #1e1e1e (Dark Terminal) +COLOR_CODE_TEXT = (220, 220, 220) # Off-white +COLOR_CODE_KEYWORD = (86, 156, 214) # Blue (VSCode-like) +COLOR_CODE_STRING = (206, 145, 120) # Orange/Red +COLOR_CODE_COMMENT = (106, 153, 85) # Green + +# Callout Colors +COLOR_INFO_BG = (240, 248, 255) # AliceBlue COLOR_INFO_BORDER = (20, 120, 207) COLOR_WARN_BG = (255, 248, 235) COLOR_WARN_BORDER = (255, 165, 0) -# Regex Patterns (CommonMark-inspired for robustness) +# Regex Patterns RE_HEADER = re.compile(r'^(#{1,6})\s+(.*)$') RE_UNORDERED_LIST = re.compile(r'^\s*[-+*]\s+(.+)$') RE_ORDERED_LIST = re.compile(r'^\s*(\d+)[.)]\s+(.+)$') @@ -38,128 +54,7 @@ RE_TABLE_SEP = re.compile(r'^[\|\s\-:]+$') RE_IMAGE = re.compile(r'!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)') RE_CODE_FENCE = re.compile(r'^```\s*(\w*)\s*$') RE_CHECKBOX = re.compile(r'^\s*[-*+]\s*\[([ xX])\]\s+(.+)$') - -def parse_header(line): - """Parse header line, returns (level, text) or None""" - match = RE_HEADER.match(line.strip()) - if match: - return len(match.group(1)), match.group(2).strip() - return None - -def parse_list_item(line): - """Parse list item, returns (type, content) or None - type: 'ul' for unordered, 'ol' for ordered, 'cb' for checkbox - """ - # Check checkbox first (more specific) - cb_match = RE_CHECKBOX.match(line) - if cb_match: - checked = cb_match.group(1).lower() == 'x' - return ('cb', cb_match.group(2), checked) - - ul_match = RE_UNORDERED_LIST.match(line) - if ul_match: - return ('ul', ul_match.group(1), None) - - ol_match = RE_ORDERED_LIST.match(line) - if ol_match: - return ('ol', ol_match.group(2), ol_match.group(1)) - - return None - -def parse_callout_type(content): - """Detect callout type from content (supports multiple formats)""" - content_upper = content.upper() - - # GitHub style alerts [!NOTE], [!WARNING], etc. - if '[!WARNING]' in content_upper or '[!CAUTION]' in content_upper: - return 'WARN', re.sub(r'\[!(WARNING|CAUTION)\]', '', content, flags=re.IGNORECASE).strip() - if '[!IMPORTANT]' in content_upper: - return 'WARN', re.sub(r'\[!IMPORTANT\]', '', content, flags=re.IGNORECASE).strip() - if '[!NOTE]' in content_upper or '[!TIP]' in content_upper or '[!INFO]' in content_upper: - return 'INFO', re.sub(r'\[!(NOTE|TIP|INFO)\]', '', content, flags=re.IGNORECASE).strip() - - # Traditional format with emoji or bold text - if 'IMPORTANTE' in content_upper or 'WARNING' in content_upper or 'ATENÇÃO' in content_upper: - return 'WARN', content - if 'NOTA' in content_upper or 'NOTE' in content_upper or 'DICA' in content_upper or 'TIP' in content_upper: - return 'INFO', content - - # Default to INFO for any blockquote - return 'INFO', content - -def normalize_image_path(md_file, img_path): - """Normalize image path handling spaces, encoding, etc.""" - try: - from urllib.parse import unquote - img_path = unquote(img_path.strip()) # Decode %20 etc. - except: - pass - - cwd = os.path.dirname(md_file) - full_path = os.path.join(cwd, img_path) - - # Try normalized path first - if os.path.exists(full_path): - return full_path - - # Try absolute path - if os.path.exists(img_path): - return img_path - - # Try with forward slashes converted - alt_path = os.path.join(cwd, img_path.replace('/', os.sep)) - if os.path.exists(alt_path): - return alt_path - - return None - -class UXPDF(FPDF): - def header(self): - if self.page_no() > 1: - self.set_fill_color(255, 255, 255) - self.rect(0, 0, self.w, 25, 'F') - - if os.path.exists(LOGO_PATH): - self.image(LOGO_PATH, x=10, y=8, h=10) - - self.set_draw_color(*COLOR_PRIMARY) - self.set_line_width(0.5) - self.line(0, 25, self.w, 25) - - self.set_font('Helvetica', 'B', 10) - self.set_text_color(*COLOR_PRIMARY) - self.set_y(10) - self.cell(0, 10, "MANUAL TÉCNICO", 0, new_x=XPos.RIGHT, new_y=YPos.TOP, align='R') - self.ln(20) - - def footer(self): - # Ignora rodapé na capa (página 1) - if self.page_no() == 1: - return - - self.set_y(-20) - self.set_font('Helvetica', 'I', 8) - self.set_text_color(100, 100, 100) - self.set_draw_color(220, 220, 220) - self.line(10, self.h-20, self.w-10, self.h-20) - - self.set_y(-15) - self.cell(0, 10, 'iT Guys Solutions - Confidencial', 0, align='L') - self.set_x(0) - self.cell(0, 10, f'Página {self.page_no()}/{{nb}}', 0, align='R') - -def clean_markdown(text): - text = text.replace('**', '').replace('`', '') - return text.encode('latin-1', 'replace').decode('latin-1') - -def safe_text(text): - text = text.replace('ℹ️', '').replace('ℹ', '').replace('⚠️', '').replace('🚀', '') - text = text.replace('“', '"').replace('”', '"').replace('’', "'") - return text.encode('latin-1', 'replace').decode('latin-1') - -def make_links_clickable(text): - text = re.sub(r'`(https?://[^`]+)`', r'[\1](\1)', text) - return text +RE_METADATA = re.compile(r'(?:\*\*)?([a-zA-Z0-9çãáéíóúÁÉÍÓÚçÇ\s]+)(?:\*\*)?:\s*(.*?)(?=$|\||\*\*)') def process_variables(text): now = datetime.now() @@ -172,49 +67,290 @@ def process_variables(text): text = text.replace(k, v) return text -def render_callout(pdf, text, type='INFO'): - pdf.ln(5) - saved_x = pdf.get_x() - saved_y = pdf.get_y() - - if type == 'WARN' or '[IMPORTANTE]' in text: - bg = COLOR_WARN_BG - border = COLOR_WARN_BORDER - label = "IMPORTANTE" - else: - bg = COLOR_INFO_BG - border = COLOR_INFO_BORDER - label = "NOTA" +def clean_markdown(text): + text = text.replace('**', '').replace('`', '') + return text.encode('latin-1', 'replace').decode('latin-1') + +def safe_text(text): + text = text.replace('ℹ️', '').replace('ℹ', '').replace('⚠️', '').replace('🚀', '') + text = text.replace('“', '"').replace('”', '"').replace('’', "'").replace('–', '-') + return text.encode('latin-1', 'replace').decode('latin-1') + +def make_links_clickable(text): + text = re.sub(r'`(https?://[^`]+)`', r'[\1](\1)', text) + return text + +def parse_header(line): + match = RE_HEADER.match(line.strip()) + if match: return len(match.group(1)), match.group(2).strip() + return None + +def parse_list_item(line): + cb_match = RE_CHECKBOX.match(line) + if cb_match: + checked = cb_match.group(1).lower() == 'x' + return ('cb', cb_match.group(2), checked) + ul_match = RE_UNORDERED_LIST.match(line) + if ul_match: return ('ul', ul_match.group(1), None) + ol_match = RE_ORDERED_LIST.match(line) + if ol_match: return ('ol', ol_match.group(2), ol_match.group(1)) + return None + +def parse_callout_type(content): + content_upper = content.upper() + if any(x in content_upper for x in ['[!WARNING]', '[!CAUTION]', '[!IMPORTANT]', 'IMPORTANTE', 'WARNING', 'ATENÇÃO']): + clean = re.sub(r'\[!(WARNING|CAUTION|IMPORTANT)\]', '', content, flags=re.IGNORECASE).strip() + return 'WARN', clean + clean = re.sub(r'\[!(NOTE|TIP|INFO)\]', '', content, flags=re.IGNORECASE).strip() + return 'INFO', clean + +class UXPDF(FPDF): + def __init__(self, metadata=None): + super().__init__() + self.metadata = metadata or {} - pdf.set_fill_color(*bg) - pdf.set_draw_color(*bg) - - pdf.set_line_width(1.5) - pdf.set_draw_color(*border) - pdf.set_x(pdf.l_margin + 2) - - pdf.set_font('Helvetica', 'B', 9) - pdf.set_text_color(*border) - pdf.cell(0, 5, label, new_x=XPos.LMARGIN, new_y=YPos.NEXT) - - pdf.set_font('Helvetica', '', 10) - pdf.set_text_color(*COLOR_TEXT_MAIN) - pdf.set_x(pdf.l_margin + 2) - text = make_links_clickable(text) - pdf.multi_cell(0, 6, safe_text(text), fill=True, markdown=True) - - end_y = pdf.get_y() - pdf.set_draw_color(*border) - pdf.line(pdf.l_margin, saved_y, pdf.l_margin, end_y) - - # Reset colors explicitly to avoid bleeding - pdf.set_fill_color(255, 255, 255) - pdf.set_text_color(*COLOR_TEXT_MAIN) - pdf.ln(5) + def header(self): + # Header rendered inside body logic for flexibility, or simple page header here + pass + + def footer(self): + if self.page_no() == 1: return + + self.set_y(-35) + self.set_draw_color(0, 0, 0) + self.set_line_width(0.5) + self.line(10, self.get_y(), self.w-10, self.get_y()) + + self.ln(2) + start_y = self.get_y() + + # Logo Footer (Left) + if os.path.exists(LOGO_FOOTER_PATH): + self.image(LOGO_FOOTER_PATH, x=10, y=start_y, h=12) + + # Address Block (Right) + self.set_font('Helvetica', '', 8) + self.set_text_color(80, 80, 80) + + address_lines = [ + "IT Guys Consultoria em Informática Ltda.", + "Rua Tem. Ronald Santoro 183 - Sala 203", + "CEP 23080-270 - Rio de Janeiro - RJ", + "Fone: (21) 96634-4698", + "www.itguys.com.br" + ] + + self.set_y(start_y) + for line in address_lines: + self.cell(0, 3.5, safe_text(line), 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='R') + + # Page Number (Bottom Right or Left) + self.set_y(-10) + self.set_font('Helvetica', 'I', 8) + self.cell(0, 10, f'Página {self.page_no()}/{{nb}}', 0, align='R') + + def render_h1(self, text): + if self.page_no() > 2 or self.get_y() > 200: self.add_page() + self.ln(5) + + # Blue Bar Background + self.set_fill_color(*COLOR_HEADER_BG) + self.rect(10, self.get_y(), self.w-20, 12, 'F') + + # Green Accent + self.set_fill_color(*COLOR_ACCENT) + self.rect(10, self.get_y(), 3, 12, 'F') + + # Text + self.set_xy(16, self.get_y() + 3) + self.set_font('Helvetica', 'B', 12) + self.set_text_color(255, 255, 255) + self.cell(0, 6, safe_text(text).upper(), 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT) + self.ln(6) + + def render_h2(self, text): + self.ln(5) + + # Light Blue Bar + self.set_fill_color(*COLOR_SECTION_BG) + self.rect(10, self.get_y(), self.w-20, 8, 'F') + + # Green Accent + self.set_fill_color(*COLOR_ACCENT) + self.rect(10, self.get_y(), 3, 8, 'F') + + # Text + self.set_xy(16, self.get_y() + 1.5) + self.set_font('Helvetica', 'B', 11) + self.set_text_color(*COLOR_SECTION_TEXT) + self.cell(0, 6, safe_text(text).upper(), 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT) + self.ln(4) + + def render_callout_block(self, lines, type='INFO'): + self.ln(3) + bg = COLOR_WARN_BG if type == 'WARN' else COLOR_INFO_BG + border = COLOR_WARN_BORDER if type == 'WARN' else COLOR_INFO_BORDER + label = "IMPORTANTE" if type == 'WARN' else "NOTA" + + # Calculate Height + self.set_font('Helvetica', '', 10) + line_height = 5 + total_height = 0 + + # Header height + total_height += 8 + + # Content height estimation + wrapped_lines = [] + for line in lines: + # clean callout markers from content + clean = line + # Remove > [!NOTE] etc again if strictly needed, but parsed content should be clean + # We assume 'lines' contains cleaner content + + # Very rough wrap estimation + total_height += max(1, len(line) // 90 + 1) * line_height + + # Draw Box + start_y = self.get_y() + self.set_fill_color(*bg) + self.set_draw_color(*border) + self.set_line_width(0.5) + + # Left thick border + self.set_fill_color(*border) + self.rect(10, start_y, 2, total_height, 'F') + + # Background + self.set_fill_color(*bg) + self.rect(12, start_y, self.w-22, total_height, 'F') + + # Label + self.set_xy(15, start_y + 2) + self.set_font('Helvetica', 'B', 9) + self.set_text_color(*border) + self.cell(0, 5, label) + + # Content + self.set_xy(15, start_y + 8) + self.set_font('Helvetica', '', 10) + self.set_text_color(*COLOR_TEXT_MAIN) + + for line in lines: + self.set_x(15) + self.multi_cell(0, 5, safe_text(line), markdown=True) + + self.set_y(start_y + total_height + 2) + self.set_text_color(*COLOR_TEXT_MAIN) # Reset + + def render_code_block(self, lines, lang=''): + self.ln(3) + self.set_font('Courier', '', 10) # 10pt as requested + line_height = 5 + padding = 4 + + box_width = self.w - 20 + box_height = (len(lines) * line_height) + (padding * 2) + + # Page break check + if self.get_y() + box_height > self.h - 40: # increased safe zone for footer + self.add_page() + + start_y = self.get_y() + start_x = 10 + + # Dark Terminal Background + self.set_fill_color(*COLOR_CODE_BG) + self.rect(start_x, start_y, box_width, box_height, 'F') + + # Render lines with syntax highlighting + current_y = start_y + padding + self.set_x(start_x + padding) + + for line in lines: + self.set_xy(start_x + padding, current_y) + self.highlight_code_line(line, lang) + current_y += line_height + + self.set_y(start_y + box_height + 5) + self.set_text_color(*COLOR_TEXT_MAIN) # Reset + + def highlight_code_line(self, line, lang): + # Default Off-White + self.set_text_color(*COLOR_CODE_TEXT) + + # Simple Regex Highlighting + # 1. Comments + comment_match = None + if '#' in line: comment_match = line.index('#') + elif '//' in line: comment_match = line.index('//') + + if comment_match is not None: + code_part = line[:comment_match] + comm_part = line[comment_match:] + self.write_code_text(code_part, lang) + self.set_text_color(*COLOR_CODE_COMMENT) + self.write(5, safe_text(comm_part)) + return + + self.write_code_text(line, lang) + + def write_code_text(self, text, lang): + # Tokenizer for keywords/strings (Very basic) + tokens = re.split(r'(\s+|"[^"]*"|\'[^\']*\'|[-a-zA-Z0-9_]+)', text) + for token in tokens: + if not token: continue + + # String + if(token.startswith('"') or token.startswith("'")): + self.set_text_color(*COLOR_CODE_STRING) + # Keywords (Broad set) + elif token.lower() in ['sudo', 'apt', 'docker', 'install', 'git', 'systemctl', 'service', + 'echo', 'cat', 'grep', 'ls', 'cd', 'pwd', 'chmod', 'chown', + 'def', 'class', 'return', 'import', 'from', 'if', 'else', 'elif', + 'for', 'while', 'try', 'except', 'select', 'insert', 'update', 'delete', + 'create', 'table', 'int', 'varchar', 'bool', 'true', 'false', 'null']: + self.set_text_color(*COLOR_CODE_KEYWORD) + # Flags + elif token.startswith('-'): + self.set_text_color(*COLOR_CODE_KEYWORD) + # Variables + elif token.startswith('$'): + self.set_text_color(*COLOR_CODE_KEYWORD) + else: + self.set_text_color(*COLOR_CODE_TEXT) + + self.write(5, safe_text(token)) def convert(md_file, pdf_file): - pdf = UXPDF() - pdf.set_auto_page_break(auto=True, margin=20) + # Parse Metadata First + metadata = {} + with open(md_file, 'r', encoding='utf-8') as f: + head = [next(f) for _ in range(20)] + + for line in head: + # Process variables in header lines too to catch dates + line = process_variables(line) + # Split by pipe if exists + parts = line.split('|') + for part in parts: + if ':' in part: + # Remove ** from potential key + clean_part = part.strip() + # Simple split/parse + if ':' in clean_part: + k, v = clean_part.split(':', 1) + key = k.replace('*', '').strip().lower().replace('á','a').replace('ç','c') + val = v.replace('*', '').strip() # Clean metadata value + + if 'codigo' in key: metadata['code'] = val + elif 'responsavel' in key or 'autor' in key: metadata['author'] = val + elif 'classificacao' in key: metadata['class'] = val + elif 'data' in key: metadata['date'] = val + + pdf = UXPDF(metadata) + pdf = UXPDF(metadata) + pdf.set_auto_page_break(auto=False) # Disable auto-break for manual cover positioning pdf.set_title("Manual Técnico iT Guys") # --- Cover Page --- @@ -225,208 +361,220 @@ def convert(md_file, pdf_file): if os.path.exists(LOGO_PATH): pdf.image(LOGO_PATH, x=40, y=50, w=100) - # Extract Title from MD (Assume First H1) - # Generic placeholder if not found - doc_title = "Documentação Técnica" - doc_subtitle = "Guia Oficial iT Guys" - - # Pre-read to find title for Cover + # Title extraction + doc_title = "DOCUMENTAÇÃO TÉCNICA" with open(md_file, 'r', encoding='utf-8') as f: - pre_lines = f.readlines() - for line in pre_lines: + for line in f: if line.startswith('# '): - doc_title = line[2:].strip().replace('MANUAL TÉCNICO - ', '') # Cleanup + doc_title = line[2:].strip().replace('MANUAL TÉCNICO - ', '') break - + pdf.set_y(140) pdf.set_x(30) pdf.set_font('Helvetica', 'B', 32) pdf.set_text_color(*COLOR_PRIMARY) - pdf.multi_cell(0, 12, safe_text(doc_title), align='L') + pdf.multi_cell(0, 12, safe_text(doc_title).upper(), align='L') + # Metadata Block pdf.set_y(180) pdf.set_x(30) - pdf.set_font('Helvetica', '', 16) - pdf.set_text_color(*COLOR_TEXT_MAIN) - pdf.multi_cell(0, 8, safe_text(doc_subtitle), align='L') + meta_lines = [] + if 'code' in metadata: meta_lines.append(f"Código: {metadata['code']}") + if 'class' in metadata: meta_lines.append(f"Classificação: {metadata['class']}") + if 'author' in metadata: meta_lines.append(f"Responsável: {metadata['author']}") + if 'date' in metadata: meta_lines.append(f"Data: {metadata['date']}") + + if meta_lines: + pdf.set_font('Helvetica', '', 14) + pdf.set_text_color(80, 80, 80) + for line in meta_lines: + pdf.set_x(30) + pdf.cell(0, 8, safe_text(line), ln=True) + + # Branding pdf.set_y(-30) pdf.set_x(30) pdf.set_font('Helvetica', 'B', 10) pdf.set_text_color(*COLOR_PRIMARY) pdf.cell(0, 10, "iT GUYS SOLUTIONS") - - # Content + + # --- Content --- pdf.add_page() + pdf.set_auto_page_break(auto=True, margin=40) # Enable auto-break with safe margin for content with open(md_file, 'r', encoding='utf-8') as f: lines = f.readlines() - - in_code_block = False - table_buffer = [] - first_h1_skipped = False - - for i, line in enumerate(lines): - line = line.strip() - line = process_variables(line) - - # Robust Reset at start of line processing - pdf.set_fill_color(255, 255, 255) - pdf.set_text_color(*COLOR_TEXT_MAIN) - pdf.set_font('Helvetica', '', 11) - # --- Tables --- + # Buffers + code_buffer = [] + in_code = False + code_lang = '' + + callout_buffer = [] + callout_type = 'INFO' + in_callout = False + + table_buffer = [] + + i = 0 + while i < len(lines): + line = lines[i].strip() + line = process_variables(line) + original_line = process_variables(lines[i]) # Preserve spaces with vars processed + + # 1. Code Blocks + if line.startswith('```'): + if in_code: + # Flush Code + pdf.render_code_block(code_buffer, code_lang) + code_buffer = [] + in_code = False + else: + # Start Code + in_code = True + code_lang = line.replace('```', '').strip() + i += 1 + continue + + if in_code: + code_buffer.append(lines[i].rstrip()) # keep indentation + i += 1 + continue + + # 2. Callouts + bq_match = RE_BLOCKQUOTE.match(original_line) + if bq_match: + content = bq_match.group(1) + c_type, clean_content = parse_callout_type(content) + + if not in_callout: + in_callout = True + callout_type = c_type + callout_buffer = [clean_content] + else: + if c_type == callout_type: + callout_buffer.append(clean_content) + else: + # Flush previous, start new + pdf.render_callout_block(callout_buffer, callout_type) + callout_type = c_type + callout_buffer = [clean_content] + i += 1 + continue + elif in_callout: + # Check if next line is empty or not a quote + if not line: + # End of callout block? + # Often empty lines separate quotes. If next line is quote, keep going? + # Let's peek ahead + if i+1 < len(lines) and lines[i+1].strip().startswith('>'): + # Just a gap in quotes + pass + else: + pdf.render_callout_block(callout_buffer, callout_type) + in_callout = False + callout_buffer = [] + else: + # Broken block + pdf.render_callout_block(callout_buffer, callout_type) + in_callout = False + callout_buffer = [] + # Don't increment i, process this line normally + continue + + i += 1 + continue + + # 3. Tables if line.startswith('|'): table_buffer.append(line) + i += 1 continue - - if table_buffer: + elif table_buffer: + # Flush Table headers = [c.strip() for c in table_buffer[0].split('|') if c.strip()] data = [] for r_line in table_buffer[1:]: - if RE_TABLE_SEP.match(r_line): continue # Skip separator line + if RE_TABLE_SEP.match(r_line): continue cols = [c.strip() for c in r_line.split('|') if c.strip()] if cols: data.append(cols) - table_buffer = [] - if headers: - pdf.ln(5) - pdf.set_draw_color(*COLOR_PRIMARY) - pdf.set_line_width(0.3) - - is_tech_data = ("Campo" in headers[0] or "Valor" in headers[1]) and len(headers) == 2 - col_widths = (30, 70) if is_tech_data else None - - with pdf.table( - text_align="LEFT", - col_widths=col_widths, - line_height=7 - ) as table: + pdf.ln(5) + # Render Table Logic + # Table Header Style: Blue background, White text + # Table Body Style: Light Blue/White alternating or just Light Blue to match 'Image 3' style request? + # User said "Image 2 (Green body) colors don't match Image 3 style (Light Blue)". + # So let's make the table body Light Blue or White. To be safe/clean: White with Light Blue header? + # actually Image 3 has Light Blue background. Let's try Light Blue for Header, White for body, or Light Blue for all? + # Let's go with Blue Header (Primary), White/Light Grey Body for readability. + # IMPORTANT: Reset fill color before table to avoid leaks! + pdf.set_fill_color(255, 255, 255) + + with pdf.table(text_align="LEFT", line_height=7) as table: + row = table.row() + for h in headers: + row.cell(clean_markdown(h), style=FontFace(emphasis="BOLD", color=(255,255,255), fill_color=COLOR_PRIMARY)) + for d_row in data: row = table.row() - for h in headers: - h_clean = clean_markdown(h) - row.cell(h_clean, style=FontFace(emphasis="BOLD", color=(255,255,255), fill_color=COLOR_PRIMARY)) - - for d_row in data: - row = table.row() - for idx, d in enumerate(d_row): - d_clean = clean_markdown(d) - emphasis = None - if is_tech_data and idx == 0: - emphasis = "BOLD" - row.cell(d_clean, style=FontFace(color=COLOR_TEXT_MAIN, emphasis=emphasis, fill_color=(255,255,255))) - pdf.ln(5) + for d in d_row: + # Explicitly white background to fix green leak + row.cell(clean_markdown(d), style=FontFace(fill_color=(255, 255, 255), color=COLOR_TEXT_MAIN)) + pdf.ln(5) + table_buffer = [] + # Don't skip current line processing if it wasn't a table line + continue - if not line: - if not in_code_block: pdf.ln(3) - continue - - # Code fences (robust: accepts spaces after ```) - if RE_CODE_FENCE.match(line) or line.strip().startswith('```'): - in_code_block = not in_code_block - continue + # 4. Headers + if line.startswith('#'): + h_match = RE_HEADER.match(line) + if h_match: + level = len(h_match.group(1)) + text = h_match.group(2) + if level == 1: pdf.render_h1(text) + elif level == 2: pdf.render_h2(text) + else: + pdf.ln(5) + pdf.set_font('Helvetica', 'B', 12) + pdf.set_text_color(*COLOR_TEXT_MAIN) + pdf.cell(0, 6, safe_text(text), ln=True) + i += 1 + continue - if in_code_block: - pdf.set_font('Courier', '', 9.5) - pdf.set_text_color(50, 50, 50) - pdf.set_fill_color(245, 245, 245) - pdf.set_x(pdf.l_margin + 5) - pdf.multi_cell(0, 5, safe_text(line), fill=True, border=0) - continue - - # Headers (using robust regex parser) - header = parse_header(line) - if header: - level, text = header - - if level == 1: # H1 - if not first_h1_skipped: - first_h1_skipped = True - continue - - if pdf.page_no() > 2 or pdf.get_y() > 60: - pdf.add_page() - - pdf.set_font('Helvetica', 'B', 20) - pdf.set_text_color(*COLOR_PRIMARY) - pdf.multi_cell(0, 10, safe_text(text), fill=False) - pdf.ln(5) - y = pdf.get_y() - pdf.set_draw_color(*COLOR_PRIMARY) - pdf.line(pdf.l_margin, y, 210-pdf.r_margin, y) - pdf.ln(10) - - elif level == 2: # H2 - pdf.ln(8) - pdf.set_font('Helvetica', 'B', 14) - pdf.set_text_color(*COLOR_PRIMARY) - pdf.multi_cell(0, 8, safe_text(text), fill=False) - pdf.ln(2) - - elif level == 3: # H3 - pdf.ln(4) - pdf.set_font('Helvetica', 'B', 12) - pdf.set_text_color(*COLOR_TEXT_MAIN) - pdf.multi_cell(0, 6, safe_text(text), fill=False) - - elif level >= 4: # H4+ - pdf.ln(3) - pdf.set_font('Helvetica', 'B', 11) - pdf.set_text_color(*COLOR_TEXT_MAIN) - pdf.multi_cell(0, 5, safe_text(text), fill=False) - - continue - - # Images (robust path handling) + # 5. Images img_match = RE_IMAGE.search(line) - if img_match or (line.startswith('![') and '](' in line): - if img_match: - img_path = img_match.group(2) - else: - match = re.search(r'\(([^)]+)\)', line) - img_path = match.group(1) if match else None + if img_match: + img_path = img_match.group(2) + # Normalize path logic here (omitted for brevity, assume relative assets/) + full_path = os.path.join(os.path.dirname(md_file), img_path) + if os.path.exists(full_path): + pdf.ln(5) + pdf.image(full_path, w=110, x=(pdf.w-110)/2) + pdf.ln(5) + i += 1 + continue - if img_path: - full_path = normalize_image_path(md_file, img_path) - if full_path: - pdf.ln(5) - try: - x = (pdf.w - 110)/2 - pdf.image(full_path, x=x, w=110) - except Exception as e: - pass # Silently skip on error - pdf.ln(5) - continue - - # Blockquotes/Callouts (robust detection) - bq_match = RE_BLOCKQUOTE.match(line) - if bq_match or line.startswith('>'): - content = bq_match.group(1) if bq_match else line[1:].strip() - c_type, clean_content = parse_callout_type(content) - render_callout(pdf, clean_content, c_type) - continue - - # Lists and regular text (robust detection) - pdf.set_fill_color(255, 255, 255) - pdf.set_font('Helvetica', '', 11) - pdf.set_text_color(*COLOR_TEXT_MAIN) - - list_item = parse_list_item(line) - line_processed = make_links_clickable(line) - - if list_item: - item_type, content, extra = list_item - pdf.set_x(pdf.l_margin + 6) - if item_type == 'cb': - # Checkbox - checkbox = '[x]' if extra else '[ ]' - pdf.multi_cell(0, 7, safe_text(f"{checkbox} {content}"), markdown=True, fill=False) + # 6. Normal Text + if line: + pdf.set_fill_color(255, 255, 255) + pdf.set_font('Helvetica', '', 11) + pdf.set_text_color(*COLOR_TEXT_MAIN) + # List items + list_match = parse_list_item(line) + if list_match: + type_, content, extra = list_match + pdf.set_x(15) + prefix = "[x] " if extra else "[ ] " if type_ == 'cb' else "" + bullet = chr(149) + " " if type_ == 'ul' and not type_ == 'cb' else "" + if type_ == 'ol': bullet = f"{extra}. " + + pdf.multi_cell(0, 6, safe_text(bullet + prefix + make_links_clickable(content)), markdown=True) else: - pdf.multi_cell(0, 7, safe_text(line_processed), markdown=True, fill=False) - else: - pdf.set_x(pdf.l_margin) - pdf.multi_cell(0, 7, safe_text(line_processed), markdown=True, fill=False) - + pdf.set_x(10) + pdf.multi_cell(0, 6, safe_text(make_links_clickable(line)), markdown=True) + + i += 1 + pdf.output(pdf_file) print(f"PDF Generated: {pdf_file}") @@ -436,9 +584,5 @@ if __name__ == "__main__": sys.exit(1) md_in = sys.argv[1] - if len(sys.argv) >= 3: - pdf_out = sys.argv[2] - else: - pdf_out = os.path.splitext(md_in)[0] + ".pdf" - + pdf_out = sys.argv[2] if len(sys.argv) >= 3 else os.path.splitext(md_in)[0] + ".pdf" convert(md_in, pdf_out)