#!/usr/bin/env python3
"""Generate printable PDFs from guide.md files for 2nd Grade curriculum."""
import os
import re
import glob
from fpdf import FPDF
BASE = os.path.expanduser("~/Home_School/2nd Grade")
FONT_DIR = "/usr/share/fonts/truetype/dejavu"
class PDF(FPDF):
def __init__(self):
super().__init__()
self.add_font('DejaVu', '', f'{FONT_DIR}/DejaVuSans.ttf', uni=True)
self.add_font('DejaVu', 'B', f'{FONT_DIR}/DejaVuSans-Bold.ttf', uni=True)
self.add_font('DejaVuMono', '', f'{FONT_DIR}/DejaVuSansMono.ttf', uni=True)
self.add_font('DejaVuMono', 'B', f'{FONT_DIR}/DejaVuSansMono-Bold.ttf', uni=True)
self.subject = ''
self.week = ''
def header(self):
self.set_font('DejaVu', 'B', 9)
self.set_text_color(100, 100, 100)
self.cell(0, 6, f'{self.subject} • {self.week}', 0, 1, 'L')
self.set_draw_color(200, 200, 200)
self.line(10, 12, 200, 12)
self.ln(4)
def footer(self):
self.set_y(-15)
self.set_font('DejaVu', '', 8)
self.set_text_color(150, 150, 150)
self.cell(0, 10, f'Page {self.page_no()}', 0, 0, 'C')
def add_section_title(self, text):
self.set_font('DejaVu', 'B', 14)
self.set_text_color(30, 30, 30)
self.cell(0, 10, text, 0, 1, 'L')
self.set_draw_color(60, 120, 200)
self.set_line_width(0.4)
self.line(10, self.get_y(), 200, self.get_y())
self.ln(4)
def add_sub_title(self, text):
self.set_font('DejaVu', 'B', 11)
self.set_text_color(50, 50, 50)
self.cell(0, 8, text, 0, 1, 'L')
self.ln(1)
def add_sub_sub_title(self, text):
self.set_font('DejaVu', 'B', 10)
self.set_text_color(60, 60, 60)
self.cell(0, 7, text, 0, 1, 'L')
self.ln(1)
def add_body(self, text, indent=0):
self.set_font('DejaVu', '', 10)
self.set_text_color(30, 30, 30)
x = 10 + indent
w = 190 - indent
self.set_x(x)
self.multi_cell(w, 5, text)
self.ln(2)
def add_bold_body(self, text):
self.set_font('DejaVu', 'B', 10)
self.set_text_color(30, 30, 30)
self.set_x(10)
self.multi_cell(190, 5, text)
self.ln(1)
def add_column_math(self, lines):
"""Render a column math block (stacked addition/subtraction)."""
self.set_font('DejaVuMono', '', 12)
self.set_text_color(30, 30, 30)
y_start = self.get_y()
max_len = max(len(l) for l in lines) if lines else 10
# Calculate width needed
char_w = self.get_string_width('0')
block_w = (max_len + 2) * char_w
x = (210 - block_w) / 2 # center the block
self.set_draw_color(180, 180, 180)
self.set_fill_color(248, 248, 248)
# Draw background
block_h = (len(lines) + 1) * (char_w * 1.3)
self.rect(x - 5, y_start - 2, block_w + 10, block_h, 'F')
self.set_x(x)
for i, line in enumerate(lines):
self.set_y(y_start + i * (char_w * 1.3))
if '---' in line:
# Draw a line instead
self.set_draw_color(80, 80, 80)
self.set_line_width(0.6)
ly = self.get_y() + char_w * 0.8
self.line(x - 3, ly, x + block_w, ly)
else:
self.cell(block_w, char_w * 1.3, f' {line} ')
self.set_line_width(0.2)
self.ln(block_h + 4)
def add_problem_list(self, items):
"""Add numbered problem list, 2 per row when possible."""
self.set_font('DejaVu', '', 10)
self.set_text_color(30, 30, 30)
x_start = 10
col_w = 90
line_h = 6
for i, item in enumerate(items):
if i % 2 == 0:
self.set_x(x_start)
self.set_y(self.get_y())
else:
self.set_x(x_start + col_w + 10)
self.set_y(self.get_y() - line_h)
# Check if we need a new page
if self.get_y() > 255:
self.add_page()
self.set_x(x_start)
self.cell(col_w, line_h, item)
self.ln(3)
def parse_guide(content, subject, week_num):
"""Parse guide.md and return a list of day sections."""
sections = []
# Match ## headers, skip overview
for m in re.finditer(r'^## (.+?)$\n(.*?)(?=^## )', content, re.MULTILINE | re.DOTALL):
title = m.group(1).strip()
body = m.group(2).strip()
if any(skip in title.lower() for skip in ["overview", "daily breakdown"]):
continue
if body.strip():
sections.append((title, body))
if not sections:
# Fallback
parts = re.split(r'^## ', content, flags=re.MULTILINE)
for part in parts:
lines = part.strip().split('\n')
if not lines:
continue
title = lines[0].strip()
if any(skip in title.lower() for skip in ["overview", "daily breakdown"]):
continue
body = '\n'.join(lines[1:]).strip()
if body:
sections.append((title, body))
return sections
def render_section(pdf, body):
"""Render a day section body into the PDF."""
# Remove emoji prefixes
body = re.sub(r'### ', '### ', body)
body = re.sub(r'^\*+(.*?)\*+', r'\1', body, flags=re.MULTILINE)
# Split into blocks
lines = body.split('\n')
i = 0
in_code = False
code_lines = []
in_problem_set = False
problem_items = []
while i < len(lines):
line = lines[i]
stripped = line.strip()
# Handle code blocks (column math)
if stripped.startswith('```'):
if in_code:
# End of code block - render as column math
clean_lines = [l.strip() for l in code_lines if l.strip()]
if clean_lines:
# Check if it looks like column math (has numbers and dashes)
has_math = any(re.search(r'[\d\s+\-]', l) for l in clean_lines)
if has_math and len(clean_lines) >= 2:
pdf.add_column_math(clean_lines)
else:
pdf.add_body(f'```\n{"\n".join(clean_lines)}\n```')
code_lines = []
in_code = False
else:
in_code = True
code_lines = []
i += 1
continue
if in_code:
code_lines.append(line)
i += 1
continue
# Headers
if stripped.startswith('### '):
pdf.add_sub_sub_title(stripped.replace('### ', ''))
i += 1
continue
elif stripped.startswith('## '):
pdf.add_sub_title(stripped.replace('## ', ''))
i += 1
continue
# Bold lines
if stripped.startswith('**') and stripped.endswith('**'):
pdf.add_bold_body(stripped.strip('*'))
i += 1
continue
# Numbered problems (collect for 2-column layout)
if re.match(r'^\d+[\.\)]\s', stripped) or re.match(r'^\d+\.\s+.*=.*___', stripped):
# Check if it's a simple problem line
if '=' in stripped or re.match(r'^\d+\.\s+.*___', stripped):
if not in_problem_set:
in_problem_set = True
problem_items.append(stripped)
i += 1
continue
else:
if in_problem_set:
pdf.add_problem_list(problem_items)
problem_items = []
in_problem_set = False
pdf.add_body(stripped)
i += 1
continue
# Set headers like "Set 1:", "Set 2:"
if re.match(r'^\*?\*Set \d+:', stripped):
if in_problem_set:
pdf.add_problem_list(problem_items)
problem_items = []
in_problem_set = False
pdf.add_sub_sub_title(stripped.strip('*'))
i += 1
continue
# Word problem headers
if re.match(r'^\*?\*(Word Problems|Guided Practice|Independent Practice|Main Lesson|Warm-Up|Strategy)', stripped):
if in_problem_set:
pdf.add_problem_list(problem_items)
problem_items = []
in_problem_set = False
pdf.add_sub_sub_title(stripped.strip('*'))
i += 1
continue
# Blank line
if not stripped:
if in_problem_set:
pdf.add_problem_list(problem_items)
problem_items = []
in_problem_set = False
i += 1
continue
# Dash list items
if stripped.startswith('- '):
text = stripped[2:].strip()
text = text.strip('*').strip()
pdf.set_x(15)
pdf.cell(5, 5, '•')
pdf.set_font('DejaVu', '', 10)
pdf.set_text_color(30, 30, 30)
pdf.multi_cell(180, 5, text)
pdf.ln(1)
i += 1
continue
# Regular text
if stripped:
text = stripped.strip('*').strip()
if text:
pdf.add_body(text)
i += 1
# Flush remaining problems
if in_problem_set and problem_items:
pdf.add_problem_list(problem_items)
def generate_pdfs():
subjects = ["Mathematics", "Phonics & Reading", "Spelling & Grammar"]
total = 0
for subject in subjects:
for week_dir in sorted(glob.glob(os.path.join(BASE, subject, "Week *"))):
week_num = os.path.basename(week_dir).replace("Week ", "")
guide_path = os.path.join(week_dir, "guide.md")
if not os.path.exists(guide_path):
continue
with open(guide_path, 'r', encoding='utf-8') as f:
content = f.read()
sections = parse_guide(content, subject, week_num)
if not sections:
# For Spelling & Grammar, split by ## sections
sections = [(m.group(1), m.group(2)) for m in
re.finditer(r'^## (.+?)\n(.*?)(?=^## )', content, re.MULTILINE | re.DOTALL)]
if not sections:
continue
print(f"\n{subject} - Week {week_num}: {len(sections)} sections")
for title, body in sections:
pdf = PDF()
pdf.subject = subject
pdf.week = f"Week {week_num}"
pdf.add_page()
pdf.add_section_title(title)
render_section(pdf, body)
# Create printables directory
printable_dir = os.path.join(week_dir, "printables")
os.makedirs(printable_dir, exist_ok=True)
# Clean filename - keep it short
safe_name = re.sub(r'[^a-zA-Z0-9_ ]', '', title)
safe_name = re.sub(r'\s+', '_', safe_name)
# Truncate if too long
if len(safe_name) > 60:
safe_name = safe_name[:60].rsplit('_', 1)[0]
filename = f"Week {week_num}_{safe_name}.pdf"
filepath = os.path.join(printable_dir, filename)
pdf.output(filepath)
total += 1
print(f" ✓ {filename} ({os.path.getsize(filepath)} bytes)")
print(f"\nDone! Generated {total} PDFs total.")
if __name__ == "__main__":
generate_pdfs()