589 lines
21 KiB
Python
589 lines
21 KiB
Python
import sys
|
||
import os
|
||
import re
|
||
from datetime import datetime
|
||
|
||
# Dependency check
|
||
try:
|
||
from fpdf import FPDF
|
||
from fpdf.enums import XPos, YPos
|
||
from fpdf.fonts import FontFace
|
||
except ImportError:
|
||
import subprocess
|
||
subprocess.check_call([sys.executable, "-m", "pip", "install", "fpdf2"])
|
||
from fpdf import FPDF
|
||
from fpdf.enums import XPos, YPos
|
||
from fpdf.fonts import FontFace
|
||
|
||
# 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 (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)
|
||
|
||
# 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
|
||
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+(.+)$')
|
||
RE_BLOCKQUOTE = re.compile(r'^>\s*(.*)$')
|
||
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+(.+)$')
|
||
RE_METADATA = re.compile(r'(?:\*\*)?([a-zA-Z0-9çãáéíóúÁÉÍÓÚçÇ\s]+)(?:\*\*)?:\s*(.*?)(?=$|\||\*\*)')
|
||
|
||
def process_variables(text):
|
||
now = datetime.now()
|
||
replacements = {
|
||
'{{DATA_ATUAL}}': now.strftime("%d/%m/%Y"),
|
||
'{{ANO}}': str(now.year)
|
||
}
|
||
for k, v in replacements.items():
|
||
if k in text:
|
||
text = text.replace(k, v)
|
||
return text
|
||
|
||
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 {}
|
||
|
||
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):
|
||
# 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 ---
|
||
pdf.add_page()
|
||
pdf.set_fill_color(*COLOR_PRIMARY)
|
||
pdf.rect(0, 0, 15, 297, 'F')
|
||
|
||
if os.path.exists(LOGO_PATH):
|
||
pdf.image(LOGO_PATH, x=40, y=50, w=100)
|
||
|
||
# Title extraction
|
||
doc_title = "DOCUMENTAÇÃO TÉCNICA"
|
||
with open(md_file, 'r', encoding='utf-8') as f:
|
||
for line in f:
|
||
if line.startswith('# '):
|
||
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).upper(), align='L')
|
||
|
||
# Metadata Block
|
||
pdf.set_y(180)
|
||
pdf.set_x(30)
|
||
|
||
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 ---
|
||
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()
|
||
|
||
# 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
|
||
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
|
||
cols = [c.strip() for c in r_line.split('|') if c.strip()]
|
||
if cols: data.append(cols)
|
||
|
||
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 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
|
||
|
||
# 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
|
||
|
||
# 5. Images
|
||
img_match = RE_IMAGE.search(line)
|
||
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
|
||
|
||
# 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.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}")
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) < 2:
|
||
print("Usage: python convert_to_pdf.py <input.md> [output.pdf]")
|
||
sys.exit(1)
|
||
|
||
md_in = sys.argv[1]
|
||
pdf_out = sys.argv[2] if len(sys.argv) >= 3 else os.path.splitext(md_in)[0] + ".pdf"
|
||
convert(md_in, pdf_out)
|