Skip to content
Closed
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ A full-stack web application for generating LaTeX-based cheat sheets. Users sele
- **Category Selection**: Select categories with checkboxes for each class (no Ctrl/Cmd needed)
- **Drag-and-Drop Reordering**: Intuitively organize your cheat sheet by dragging individual formulas or entire collapsible class groups to reorder them before generating.
- **Formula Generation**: Automatically generates formatted LaTeX for selected formulas
- **Live Preview**: Split-view interface with LaTeX code editor and PDF preview
- **Live Preview**: Split-pane interface with line-numbered LaTeX editor (45% width) and PDF preview (45% width)
- **PDF Compilation**: Compile to PDF using Tectonic LaTeX engine on the backend
- **Download Options**: Download as `.tex` source or `.pdf`
- **Auto-Save**: Progress automatically saved to browser localStorage - survives page refresh
- **Version History**: Navigate through last 3 compiled versions with back/forward buttons

### Formatting Options
- **Column Layout**: Single, two, or three column layouts
- **Column Layout**: Single, two, or three column layouts with auto-scaling formulas that fit within column boundaries
- **Margins**: Adjustable page margins
- **Font Size**: Configurable font scaling (8pt - 14pt)
- **Spacing**: Adjustable spacing between formula sections

### Database Features
- **Templates**: Save and manage reusable LaTeX templates
Expand All @@ -43,6 +46,7 @@ A full-stack web application for generating LaTeX-based cheat sheets. Users sele
│ ├── api/ # Main API app
│ │ ├── views.py # API endpoints
│ │ ├── models.py # Database models
│ │ ├── latex_utils.py # LaTeX generation utilities
│ │ ├── urls.py # URL routing
│ │ ├── serializers.py # DRF serializers
│ │ ├── tests.py # Test suite
Expand Down
82 changes: 77 additions & 5 deletions backend/api/latex_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,85 @@
\end{document}
"""

def build_latex_for_formulas(selected_formulas):
# Font size to LaTeX size command mapping for cheat sheet density
FONT_SIZE_MAP = {
"8pt": "\\tiny",
"9pt": "\\scriptsize",
"10pt": "\\footnotesize",
"11pt": "\\small",
"12pt": "\\normalsize",
}
Comment on lines +27 to +34
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI exposes “Text Size (10pt)” options, but FONT_SIZE_MAP maps "10pt" to \footnotesize (and "12pt" to \normalsize). Combined with \documentclass[{font_size}], this likely makes the effective font smaller than the selected size and makes the labels misleading. Consider mapping 10pt -> \normalsize (and scaling others accordingly), or relabeling the UI to match the actual density preset.

Copilot uses AI. Check for mistakes.

# Spacing presets: (section_before, section_after, subsection_before, subsection_after, formula_spacing)
SPACING_MAP = {
"tiny": ("1pt", "0.5pt", "0.5pt", "0.25pt", "1pt"),
"small": ("2pt", "1pt", "1pt", "0.5pt", "2pt"),
"medium": ("3pt", "1.5pt", "1.5pt", "0.75pt", "3pt"),
"large": ("4pt", "2pt", "2pt", "1pt", "4pt"),
}


def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in", spacing="large"):
"""
Build a dynamic LaTeX header based on user-selected options.
"""
size_command = FONT_SIZE_MAP.get(font_size, "\\footnotesize")
sec_before, sec_after, subsec_before, subsec_after, _ = SPACING_MAP.get(spacing, SPACING_MAP["large"])

header_lines = [
f"\\documentclass[{font_size},fleqn]{{article}}",
f"\\usepackage[margin={margins}]{{geometry}}",
"\\usepackage{amsmath, amssymb}",
"\\usepackage{enumitem}",
"\\usepackage{multicol}",
"\\usepackage{titlesec}",
"\\usepackage{adjustbox}", # For auto-scaling equations to fit column width
"",
"\\setlength{\\mathindent}{0pt}",
"\\setlist[itemize]{noitemsep, topsep=0pt, leftmargin=*}",
"\\pagestyle{empty}",
"",
"\\titleformat{\\section}{\\normalfont\\footnotesize\\bfseries}{}{0pt}{}",
"\\titleformat{\\subsection}{\\normalfont\\scriptsize\\bfseries}{}{0pt}{}",
f"\\titlespacing*{{\\section}}{{0pt}}{{{sec_before}}}{{{sec_after}}}",
f"\\titlespacing*{{\\subsection}}{{0pt}}{{{subsec_before}}}{{{subsec_after}}}",
"",
"\\begin{document}",
size_command,
]

if columns > 1:
header_lines.append(f"\\begin{{multicols}}{{{columns}}}")
header_lines.append("\\raggedcolumns")

header_lines.append("")
return "\n".join(header_lines)


def build_dynamic_footer(columns=2):
"""
Build a dynamic LaTeX footer based on user-selected options.
"""
footer_lines = []

if columns > 1:
footer_lines.append("\\end{multicols}")

footer_lines.append("\\end{document}")
return "\n".join(footer_lines)


def build_latex_for_formulas(selected_formulas, columns=2, font_size="10pt", margins="0.25in", spacing="large"):
"""
Given a list of selected formulas (each with class_name, category, name, latex),
build a complete LaTeX document.
"""
header = build_dynamic_header(columns, font_size, margins, spacing)
footer = build_dynamic_footer(columns)
_, _, _, _, formula_gap = SPACING_MAP.get(spacing, SPACING_MAP["large"])

if not selected_formulas:
return LATEX_HEADER + LATEX_FOOTER
return header + footer

body_lines = []
current_class = None
Expand Down Expand Up @@ -74,14 +146,14 @@ def build_latex_for_formulas(selected_formulas):
# Escape special LaTeX characters in the formula name
escaped_name = name.replace("\\", "\\textbackslash ").replace("&", "\\&").replace("%", "\\%").replace("#", "\\#").replace("_", "\\_").replace("^", "\\textasciicircum ").replace("{", "\\{").replace("}", "\\}")
body_lines.append("\\textbf{" + escaped_name + "}")
body_lines.append("\\[ " + latex + " \\]")
body_lines.append("\\\\[4pt]")
body_lines.append("\\[ \\adjustbox{max width=\\linewidth}{$\\displaystyle " + latex + "$} \\]")
body_lines.append(f"\\\\[{formula_gap}]")

if in_flushleft:
body_lines.append(r"\end{flushleft}")

body = "\n".join(body_lines)
return LATEX_HEADER + body + LATEX_FOOTER
return header + body + footer

def compile_latex_to_pdf(content):
"""
Expand Down
19 changes: 13 additions & 6 deletions backend/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,26 @@ def get_classes(request):
def generate_sheet(request):
"""
POST /api/generate-sheet/
Accepts { "formulas": [...] }
Accepts { "formulas": [...], "columns": 2, "font_size": "10pt", "margins": "0.25in", "spacing": "large" }
Each formula: { "class": "ALGEBRA I", "category": "Linear Equations", "name": "Slope Formula" }
Or for special classes (like UNIT CIRCLE): { "class": "UNIT CIRCLE", "name": "Unit Circle (Key Angles)" }
Returns { "tex_code": "..." }
"""
selected = request.data.get("formulas", [])
columns = request.data.get("columns", 2)
font_size = request.data.get("font_size", "10pt")
margins = request.data.get("margins", "0.25in")
spacing = request.data.get("spacing", "large")
Comment on lines +45 to +48
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

font_size, margins, and spacing are passed straight into LaTeX generation (e.g., \documentclass[...] and geometry options). These should be validated/whitelisted to known-safe values to avoid LaTeX injection or unexpected compilation behavior when the API is called directly (not just via the UI).

Copilot uses AI. Check for mistakes.
Comment on lines 44 to +48
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The endpoint now supports additional layout parameters (columns, font_size, margins, spacing), but the existing test suite likely only covers the legacy {formulas: [...]} payload. Please add/extend API tests to exercise these new parameters (including validation) and assert the generated LaTeX reflects them (e.g., \begin{multicols}{3} / geometry margin).

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +48
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

font_size, margins, and spacing are passed straight into LaTeX generation (e.g., \documentclass[...] and geometry options). These should be validated/whitelisted to known-safe values to avoid LaTeX injection or unexpected compilation behavior when the API is called directly (not just via the UI).

Copilot uses AI. Check for mistakes.
Comment on lines 44 to +48
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The endpoint now supports additional layout parameters (columns, font_size, margins, spacing), but the existing test suite likely only covers the legacy {formulas: [...]} payload. Please add/extend API tests to exercise these new parameters (including validation) and assert the generated LaTeX reflects them (e.g., \begin{multicols}{3} / geometry margin).

Copilot uses AI. Check for mistakes.

if not selected:
return Response({"error": "No formulas selected"}, status=400)

# Get formula details from formula_data
try:
columns = int(columns)
except (TypeError, ValueError):
columns = 2
columns = max(1, min(3, columns))

formula_data = get_formula_data()
selected_formulas = []

Expand All @@ -55,14 +64,12 @@ def generate_sheet(request):
category = sel.get("category")
name = sel.get("name")

# Check if this is a special class (no categories)
if is_special_class(class_name):
# Get the formula directly for special classes
formula = get_special_class_formula(class_name)
if formula:
selected_formulas.append({
"class_name": class_name,
"category": class_name, # Use class name as category for special
"category": class_name,
"name": formula["name"],
"latex": formula["latex"]
})
Expand All @@ -82,7 +89,7 @@ def generate_sheet(request):
if not selected_formulas:
return Response({"error": "No valid formulas found"}, status=400)

tex_code = build_latex_for_formulas(selected_formulas)
tex_code = build_latex_for_formulas(selected_formulas, columns, font_size, margins, spacing)
return Response({"tex_code": tex_code})


Expand Down
Loading
Loading