#!/usr/bin/env python3
"""
Shared Helper Functions for ELA Week Generators
Provides all CSS, HTML helper functions, and layout utilities
used across week generator scripts.
Usage:
from helpers import (
CSS, header, sectitle, secnote, write_line, copy_line, page_break,
word_bank, fill_blank_with_line, word_search, remember_box, fb
)
"""
import random
# =====================================================================
# SHARED CSS
# =====================================================================
CSS = """
@page { size: letter; margin: 0.7in 0.85in; }
body { font-family: 'Comic Neue', 'Comic Sans MS', sans-serif; color: #222; background: #fff; margin: 0; line-height: 1.4; }
p { orphans: 3; widows: 3; }
header { display: flex; justify-content: space-between; align-items: baseline; border-bottom: 3px solid #26A69A; padding-bottom: 6px; margin-bottom: 12px; }
header h1 { font-size: 17px; margin: 0; color: #26A69A; }
header .subtitle { font-size: 12px; color: #666; }
header .name-line { font-size: 12px; }
h2 { font-size: 14px; color: #26A69A; margin: 14px 0 4px 0; border-bottom: 2px solid #B2DFDB; padding-bottom: 2px; break-after: avoid; }
h2.coral { color: #FF7043; border-bottom-color: #FFAB91; }
.note { background: #E8F5E9; border-left: 4px solid #26A69A; padding: 6px 10px; border-radius: 4px; font-size: 11.5px; margin: 4px 0 10px 0; color: #333; break-inside: avoid; }
.activity { break-inside: avoid; page-break-inside: avoid; }
.word-grid { width: 100%; border-collapse: separate; border-spacing: 3px; }
.word-grid td { vertical-align: top; }
.word-card { border: 1.5px solid #B2DFDB; border-radius: 6px; padding: 4px 6px; background: #FAFAFA; break-inside: avoid; }
.word-card .word { font-size: 12px; font-weight: bold; color: #26A69A; margin-bottom: 1px; }
.word-card .def { font-size: 9px; color: #555; line-height: 1.15; }
.write-line {
position: relative; height: 22px; margin: 2px 0; width: 100%;
}
.write-line-sky {
position: absolute; left: 0; right: 0; top: 0;
border-top: 0.6pt solid rgb(0,0,255);
}
.write-line-ground {
position: absolute; left: 0; right: 0; bottom: 0;
border-bottom: 0.6pt solid rgb(0,0,255);
}
.write-line-fence {
position: absolute; left: 0; right: 0; bottom: 12px;
border-bottom: 0.4pt dashed rgb(255,0,0);
}
.copy-line {
position: relative; height: 22px; margin: 2px 0; width: 100%;
}
.copy-line-sky {
position: absolute; left: 0; right: 0; top: 0;
border-top: 0.6pt solid rgb(0,0,255);
}
.copy-line-ground {
position: absolute; left: 0; right: 0; bottom: 0;
border-bottom: 0.6pt solid rgb(0,0,255);
}
.copy-line-fence {
position: absolute; left: 0; right: 0; bottom: 12px;
border-bottom: 0.4pt dashed rgb(255,0,0);
}
.story-box { background: #F3E5F5; border: 1.5px solid #CE93D8; border-radius: 8px; padding: 10px; font-size: 12px; line-height: 1.5; margin: 8px 0; break-inside: avoid; }
.test-item { display: flex; align-items: center; gap: 8px; font-size: 13px; margin: 6px 0; }
.test-item .num { font-weight: bold; color: #26A69A; min-width: 20px; }
.divider { border-top: 1px dashed #ccc; margin: 12px 0; }
.fb-item { background: #E0F2F1; border: 1.5px solid #80CBC4; border-radius: 6px; padding: 6px 8px; margin-bottom: 6px; font-size: 12px; break-inside: avoid; page-break-inside: avoid; }
.fb-item p { margin: 0 0 4px 0; }
.word-bank { background: #FFF3E0; border: 2px dashed #FFB74D; border-radius: 8px; padding: 8px 12px; margin: 8px 0; }
.word-bank-label { font-size: 11px; font-weight: bold; color: #FF7043; margin-bottom: 4px; }
.word-bank-words { display: flex; flex-wrap: wrap; gap: 6px; }
.word-bank-words span { background: white; border: 1px solid #FFB74D; border-radius: 4px; padding: 2px 8px; font-size: 11px; }
.ws-grid { display: inline-block; font-family: monospace; font-size: 12px; letter-spacing: 2px; line-height: 1.4; background: #F5F5F5; border: 2px solid #B2DFDB; border-radius: 6px; padding: 6px 8px; text-align: left; }
.ws-table { border-collapse: collapse; margin: 8px auto; background: #F5F5F5; border: 2px solid #B2DFDB; break-inside: avoid; page-break-inside: avoid; }
.ws-table td { width: 28px; height: 28px; text-align: center; vertical-align: middle; font-family: monospace; font-size: 14px; font-weight: bold; border: 1px solid #B2DFDB; color: #333; }
.ws-words { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 8px 0; justify-content: center; }
.ws-words span { font-size: 11px; background: #E0F2F1; border: 1px solid #80CBC4; border-radius: 3px; padding: 2px 6px; }
.mystery-box { background: #FFF8E1; border: 2px solid #FFD54F; border-radius: 8px; padding: 10px; margin: 8px 0; break-inside: avoid; }
.mystery-box .title { font-size: 13px; font-weight: bold; color: #FF7043; margin-bottom: 6px; }
.mystery-box li { font-size: 11px; margin: 3px 0; }
tr { break-inside: avoid; }
"""
# =====================================================================
# HTML HELPER FUNCTIONS
# =====================================================================
def header(title, subtitle=""):
"""Page header with title, subtitle, and name line."""
return (f'<header>'
f'<h1>{title}</h1>'
f'<div class="subtitle">{subtitle}</div>'
f'<div class="name-line">Name: ________________</div>'
f'</header>')
def sectitle(text, color="teal"):
"""Section title with optional coral/teal color."""
cls = "coral" if color == "coral" else ""
return f'<h2 class="{cls}">{text}</h2>'
def secnote(text):
"""Section note box with green accent."""
return f'<div class="note">{text}</div>'
def write_line():
"""Sky/fence/ground handwriting guide line."""
return '<div class="write-line"><div class="write-line-sky"></div><div class="write-line-fence"></div><div class="write-line-ground"></div></div>'
def copy_line():
"""Copy practice line (same as write_line but with copy-line class)."""
return '<div class="copy-line"><div class="copy-line-sky"></div><div class="copy-line-fence"></div><div class="copy-line-ground"></div></div>'
def page_break():
"""CSS page break."""
return '<div style="page-break-after:always;"></div>'
def fb(text):
"""Fill-in-blank item with writing line."""
return f'<div class="fb-item"><p>{text}</p>{write_line()}</div>'
# =====================================================================
# ACTIVITY BUILDERS
# =====================================================================
def word_bank(words, seed=99):
"""Word bank box with shuffled words."""
rnd = random.Random(seed)
shuffled = list(words)
rnd.shuffle(shuffled)
word_spans = "".join(f"<span>{w}</span>" for w in shuffled)
return (f'<div class="word-bank">'
f'<div class="word-bank-label">Word Bank</div>'
f'<div class="word-bank-words">{word_spans}</div>'
f'</div>')
def fill_blank_with_line(sentence):
"""Fill-in-blank sentence with answer line."""
return (f'<div class="fb-item">'
f'<p>{sentence}</p>'
f'{write_line()}'
f'</div>')
def word_search(word_list, rows=14, cols=14, seed=42):
"""Generate a word search puzzle grid."""
rnd = random.Random(seed)
grid = [[""] * cols for _ in range(rows)]
directions = [(0,1),(1,0),(1,1),(1,-1),(0,-1),(-1,0),(-1,-1),(-1,1)]
for word in word_list:
upper_word = word.upper()
placed = False
attempts = 0
while not placed and attempts < 500:
attempts += 1
dr, dc = rnd.choice(directions)
sr, sc = rnd.randint(0, rows-1), rnd.randint(0, cols-1)
er, ec = sr + dr*(len(upper_word)-1), sc + dc*(len(upper_word)-1)
if er < 0 or er >= rows or ec < 0 or ec >= cols:
continue
ok = True
for i, ch in enumerate(upper_word):
r, c = sr + dr*i, sc + dc*i
if grid[r][c] and grid[r][c] != ch:
ok = False
break
if ok:
for i, ch in enumerate(upper_word):
grid[sr + dr*i][sc + dc*i] = ch
placed = True
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for r in range(rows):
for c in range(cols):
if not grid[r][c]:
grid[r][c] = rnd.choice(letters)
cell_html = ""
for row in grid:
cell_html += "<tr>"
for ch in row:
cell_html += f'<td>{ch}</td>'
cell_html += "</tr>"
word_list_html = "".join(f"<span>{w.upper()}</span>" for w in word_list)
return (f'<div class="ws-words">{word_list_html}</div>'
f'<table class="ws-table"><tbody>{cell_html}</tbody></table>')
def remember_box(title, content):
"""Remember This! reminder box."""
return (f'<div class="story-box" style="background:#E8F5E9;border-color:#A5D6A7;">'
f'<strong style="color:#2E7D32;">{title}</strong><br>'
f'{content}'
f'</div>')
def word_names(words):
"""Extract word names from word tuples (supports 3-tuple and 4-tuple)."""
return [w[0] for w in words]
def word_definitions(words):
"""Extract definitions from word tuples. 3-tuple: index 2, 4-tuple: index 3."""
return [w[3] if len(w) >= 4 else w[2] for w in words]
def context_clue_sentence(sentence, word, highlight=True):
"""Context clue sentence with optional word highlighting."""
if highlight:
highlighted = sentence.replace(word, f'<strong style="color:#26A69A;">{word}</strong>')
else:
highlighted = sentence
return f'<div class="fb-item"><p>{highlighted}</p>{write_line()}</div>'
def context_clue_cards(sentences_words):
"""Context clue cards for Monday - list of (sentence, word) pairs."""
html = ""
for sentence, word in sentences_words:
highlighted = sentence.replace(word, f'<strong style="color:#26A69A;">{word}</strong>')
html += f'<div class="fb-item"><p>{highlighted}</p><div style="font-size:10.5px;color:#888;margin-top:2px;">What does <strong>{word}</strong> mean here? ________________</div></div>'
return html
def paragraph_parts_box(title, intro_text, example_parts):
"""Paragraph parts explanation box with labeled parts.
example_parts: list of (label, text, description) tuples.
"""
parts_html = ""
colors = {"Topic": "#E3F2FD", "Detail": "#E8F5E9", "Conclusion": "#FFF3E0"}
labels = {"Topic": "#1565C0", "Detail": "#2E7D32", "Conclusion": "#E65100"}
for label, text, desc in example_parts:
bg = colors.get(label, "#F5F5F5")
clr = labels.get(label, "#333")
parts_html += (f'<div style="margin:4px 0;padding:6px 8px;background:{bg};border-radius:4px;border-left:3px solid {clr};">'
f'<div style="font-size:10px;font-weight:bold;color:{clr};margin-bottom:2px;">{label}:</div>'
f'<div style="font-size:11px;font-style:italic;margin-bottom:2px;">{text}</div>'
f'<div style="font-size:9px;color:#888;">{desc}</div>'
f'</div>')
return (f'<div style="margin:8px 0;padding:8px 10px;background:#E8F5E9;border:2px solid #A5D6A7;border-radius:6px;">'
f'<div style="font-weight:bold;font-size:12px;color:#2E7D32;margin-bottom:4px;">{title}</div>'
f'<div style="font-size:11px;margin-bottom:6px;">{intro_text}</div>'
f'{parts_html}'
f'</div>')
def paragraph_label_activity(paragraphs):
"""Paragraph labeling activity - students identify topic/detail/conclusion.
paragraphs: list of (sentence, label) tuples where label is Topic/Detail/Conclusion.
"""
html = ""
for sentence, label in paragraphs:
html += f'<div style="margin:4px 0;font-size:12px;"><strong>__{label}__</strong>: {sentence}</div>'
return html