Zum Inhalt

latex

academic_doc_generator.core.latex

LaTeX generation and helper functions.

compile_latex_to_pdf(tex_path, output_dir=None, engine='lualatex')

Compile a LaTeX file into a PDF using the specified engine.

Parameters:

Name Type Description Default
tex_path str

Path to the .tex file.

required
output_dir str

Directory for the PDF. Defaults to same as tex file.

None
engine str

"lualatex" or "pdflatex"

'lualatex'

Returns:

Name Type Description
str str

Path to the generated PDF, or an empty string if compilation fails.

Source code in src/academic_doc_generator/core/latex.py
def compile_latex_to_pdf(
    tex_path: str, output_dir: Optional[str] = None, engine: str = "lualatex"
) -> str:
    """Compile a LaTeX file into a PDF using the specified engine.

    Args:
        tex_path (str): Path to the .tex file.
        output_dir (str, optional): Directory for the PDF. Defaults to same as tex file.
        engine (str, optional): "lualatex" or "pdflatex"

    Returns:
        str: Path to the generated PDF, or an empty string if compilation fails.
    """
    if output_dir is None:
        output_dir = os.path.dirname(tex_path)

    cmd = [
        engine,
        "-interaction=nonstopmode",
        f"-output-directory={output_dir}",
        tex_path,
    ]

    try:
        subprocess.run(cmd, check=True)
    except subprocess.CalledProcessError as e:
        print(f"❌ Error: LaTeX compilation failed for {tex_path}")
        print(f"   Command: {' '.join(cmd)}")
        print(f"   Exit status: {e.returncode}")
        return ""
    except FileNotFoundError:
        print(f"❌ Error: LaTeX engine '{engine}' not found. Please ensure it is installed.")
        return ""

    pdf_path = os.path.join(output_dir, os.path.splitext(os.path.basename(tex_path))[0] + ".pdf")
    return pdf_path

concatenate_comments(results, language, verbose=False)

Concatenate rewritten comments into a LaTeX-formatted string.

Each comment is prefixed with the page number and separated by two LaTeX line breaks (\ \) for readability.

Parameters:

Name Type Description Default
results dict

Dictionary mapping page numbers to lists of rewritten comment dictionaries (as returned by rewrite_comments_in_pdf).

required
language str

Language of the comments ("German" or "English") to determine whether "Seite" or "page" is used as the prefix.

required
verbose bool

If True, prints the concatenated comments. Defaults to False.

False

Returns:

Name Type Description
str str

A LaTeX-ready string with all rewritten comments, separated by

str

two line breaks and labeled with their page numbers.

Source code in src/academic_doc_generator/core/latex.py
def concatenate_comments(
    results: dict[int, list[dict]], language: str, verbose: bool = False
) -> str:
    """Concatenate rewritten comments into a LaTeX-formatted string.

    Each comment is prefixed with the page number and separated by two
    LaTeX line breaks (\\\\ \\\\) for readability.

    Args:
        results (dict): Dictionary mapping page numbers to lists of rewritten
            comment dictionaries (as returned by `rewrite_comments_in_pdf`).
        language (str): Language of the comments ("German" or "English") to
            determine whether "Seite" or "page" is used as the prefix.
        verbose (bool, optional): If True, prints the concatenated comments.
            Defaults to False.

    Returns:
        str: A LaTeX-ready string with all rewritten comments, separated by
        two line breaks and labeled with their page numbers.
    """
    seite_page = return_seite_page(language)

    questions = " \\\\\n\\\\\n".join(
        f"{seite_page} {page}: {item['rewritten']}"
        for page, items in results.items()
        for item in items
    )

    if verbose:
        print(questions)

    return questions

create_formal_letter_tex(filename, recipient, subject, title, author, summary, first_examiner, second_examiner, examiner_email, questions, place='Gummersbach', date='\\today', gemini_emark=None)

Create a LaTeX file for a formal letter with TH Köln footer.

Parameters:

Name Type Description Default
filename str

Output path for the LaTeX file.

required
recipient str

Recipient of the letter.

required
subject str

Subject line.

required
title str

Thesis title.

required
author str

Author name and matriculation number.

required
summary str

summary of the thesis.

required
first_examiner str

name of first examiner.

required
second_examiner str

name of second examiner.

required
examiner_email str

email of first examiner.

required
questions str

questions from first examiner.

required
place str

Place of issue. Defaults to "Gummersbach".

'Gummersbach'
date str

Date string. Defaults to LaTeX \today.

'\\today'
gemini_emark Optional[str]

Optional LaTeX-formatted Gemini evaluation text.

None

Returns:

Type Description
None

None. Writes the output to the specified filename.

Raises:

Type Description
OSError

If the file cannot be written.

Source code in src/academic_doc_generator/core/latex.py
def create_formal_letter_tex(
    filename: str,
    recipient: str,
    subject: str,
    title: str,
    author: str,
    summary: str,
    first_examiner: str,
    second_examiner: str,
    examiner_email: str,
    questions: str,
    place: str = "Gummersbach",
    date: str = r"\today",
    gemini_emark: Optional[str] = None,
) -> None:
    """Create a LaTeX file for a formal letter with TH Köln footer.

    Args:
        filename: Output path for the LaTeX file.
        recipient: Recipient of the letter.
        subject: Subject line.
        title: Thesis title.
        author: Author name and matriculation number.
        summary: summary of the thesis.
        first_examiner: name of first examiner.
        second_examiner: name of second examiner.
        examiner_email: email of first examiner.
        questions: questions from first examiner.
        place: Place of issue. Defaults to "Gummersbach".
        date: Date string. Defaults to LaTeX \\today.
        gemini_emark: Optional LaTeX-formatted Gemini evaluation text.

    Returns:
        None. Writes the output to the specified filename.

    Raises:
        OSError: If the file cannot be written.
    """
    # Füge Gemini-Bewertung hinzu, falls vorhanden
    gemini_section = ""
    if gemini_emark:
        gemini_section = f"\n\n{gemini_emark}\n"

    rendered_output = rf"""
\documentclass[11pt,ngerman,parskip=full]{{scrlttr2}}
\usepackage{{fontspec}}
\setmainfont{{Latin Modern Roman}}
\usepackage[ngerman]{{babel}}
\usepackage{{geometry}}
\geometry{{a4paper, top=25mm, left=25mm, right=25mm, bottom=30mm}}
\usepackage{{url}}

% Sender info
\setkomavar{{fromname}}{{{first_examiner}}}
\setkomavar{{fromaddress}}{{Steinmüllerallee 1\\51643 Gummersbach}}
\setkomavar{{fromphone}}{{+49 2261-8196-6204}}
\setkomavar{{fromemail}}{{{examiner_email}}}
\setkomavar{{place}}{{{place}}}
\setkomavar{{date}}{{{date}}}
\setkomavar{{signature}}{{{first_examiner}}}
\setkomavar{{subject}}{{{escape_latex_text(subject)}}}

% Footer
\setkomavar{{firstfoot}}{{%
  \parbox[t]{{\textwidth}}{{\footnotesize
    Technische Hochschule Köln, Campus Gummersbach \\
    Sitz des Präsidiums: Claudiusstrasse 1, 50678 Köln \\
    www.th-koeln.de \\
    Steuer-Nr.: 214/5817/3402 - USt-IdNr.: DE 122653679 \\
    Bankverbindung: Sparkasse KölnBonn \\
    IBAN: DE34 3705 0198 1900 7098 56 - BIC: COLSDE33
  }}
}}

\begin{{document}}

\begin{{letter}}{{{escape_latex_text(recipient)}}}

\opening{{Sehr geehrte Damen und Herren,}}

Bewertung folgender Thesis:\\[1ex]

\textbf{{Titel:}} {escape_latex_text(title)} \\[1ex]
\textbf{{Autor:}} {escape_latex_text(author)} \\[2ex]

\textbf{{Zusammenfassung der Thesis:}} \\

{summary}


\textbf{{Protokoll des Kolloquiums:}}\\[1ex]

\textbf{{Fragen {first_examiner}:}}\\

{questions}\\


\textbf{{Fragen {second_examiner}:}}\\

\textbf{{Vortrag:}} xx Minuten\\

Bewertung des Vortrags:

1. Inhaltliche Qualität \& Struktur:

Kriterien:
\begin{{itemize}}
\item Verständlichkeit von Ziel, Problemstellung und Ergebnissen
\item Fachliche Richtigkeit
\item Logischer Aufbau, klarer roter Faden, sinnvolle Schwerpunktsetzung
\item Einhaltung der Zeit
\end{{itemize}}

Bewertung der Kriterien:

\begin{{itemize}}
\item sehr gut
\item gut
\item befriedigend
\item ausreichend
\end{{itemize}}

2. Darstellung \& Visualisierung:

Kriterien:
\begin{{itemize}}
\item Unterstützung des Vortrags durch Folien und Visualisierungen
\item Übersichtlichkeit und Angemessenheit der Gestaltung
\item Verständliche Vermittlung auch komplexer Inhalte
\end{{itemize}}

Bewertung der Kriterien:

\begin{{itemize}}
\item sehr gut
\item gut
\item befriedigend
\item ausreichend
\end{{itemize}}

3. Präsentation \& Auftreten:

Kriterien:
\begin{{itemize}}
\item Freier, sicherer und verständlicher Vortrag (Sprache, Tempo, Körpersprache)
\item Souveräner Umgang mit Fragen
\item Kritische Reflexion der eigenen Arbeit (Stärken, Grenzen, Ausblick)
\end{{itemize}}

Bewertung der Kriterien:

\begin{{itemize}}
\item sehr gut
\item gut
\item befriedigend
\item ausreichend
\end{{itemize}}

Demo:
\begin{{itemize}}
\item ja, live
\item ja, live, aber Fehlerhaft/nicht so gut
\item ja, Video
\item nein
\item nicht möglich
\end{{itemize}}

Fragen konnten beantwortet werden:
\begin{{itemize}}
\item sehr gut
\item sehr gut, manche gut
\item gut
\item gut, manche nicht so gut
\item viele nicht so gut oder gar nicht
\end{{itemize}}

.\\[2ex]

Dauer des Kolloquiums: 45 Minuten
{gemini_section}

\closing{{Mit freundlichen Grü{{\ss}}en}}

\end{{letter}}

\end{{document}}
"""
    with open(filename, "w", encoding="utf-8") as f:
        f.write(rendered_output)
    print(f"LaTeX file created: {filename}")

escape_for_latex(text, preserve_latex=True)

Legacy wrapper for LaTeX escaping.

Parameters:

Name Type Description Default
text str

Input text.

required
preserve_latex bool

Whether to preserve LaTeX commands.

True

Returns:

Type Description
str

Escaped text.

Source code in src/academic_doc_generator/core/latex.py
def escape_for_latex(text: str, preserve_latex: bool = True) -> str:
    """Legacy wrapper for LaTeX escaping.

    Args:
        text: Input text.
        preserve_latex: Whether to preserve LaTeX commands.

    Returns:
        Escaped text.
    """
    if preserve_latex:
        return escape_latex_with_commands(text)
    return escape_latex_text(text)

escape_latex_text(text) cached

Escape text for safe LaTeX insertion (no LaTeX commands preserved).

Caches results for performance on repeated strings.

Parameters:

Name Type Description Default
text str

Input text to escape.

required

Returns:

Type Description
str

LaTeX-safe string.

Source code in src/academic_doc_generator/core/latex.py
@lru_cache(maxsize=1024)
def escape_latex_text(text: str) -> str:
    """Escape text for safe LaTeX insertion (no LaTeX commands preserved).

    Caches results for performance on repeated strings.

    Args:
        text: Input text to escape.

    Returns:
        LaTeX-safe string.
    """
    if text is None:
        return ""

    text = unicodedata.normalize("NFKC", text)

    # Remove invisible chars (soft hyphen, zero-width spaces, etc.)
    text = _remove_invisible_chars(text)

    # Replace dash-like characters with plain ASCII hyphen
    text = _normalize_dashes(text, replacement="-")

    # Define all replacements including specials and sharp s
    replacements = {
        "\\": r"\textbackslash{}",
        "&": r"\&",
        "%": r"\%",
        "$": r"\$",
        "#": r"\#",
        "_": r"\_",
        "{": r"\{",
        "}": r"\}",
        "~": r"\textasciitilde{}",
        "^": r"\textasciicircum{}",
        "„": r"``",
        "“": r"''",
        "ß": r"{\ss}",
    }

    # Use regex to perform all replacements in one pass to avoid double-escaping
    pattern = re.compile("|".join(re.escape(k) for k in replacements))
    text = pattern.sub(lambda m: replacements[m.group(0)], text)

    return text

escape_latex_with_commands(text)

Escape text while preserving LaTeX commands like \textbf{}.

Use this when the text may contain intentional LaTeX formatting.

Parameters:

Name Type Description Default
text str

Input text that may contain LaTeX commands.

required

Returns:

Type Description
str

LaTeX-safe string with commands preserved.

Source code in src/academic_doc_generator/core/latex.py
def escape_latex_with_commands(text: str) -> str:
    """Escape text while preserving LaTeX commands like \\textbf{}.

    Use this when the text may contain intentional LaTeX formatting.

    Args:
        text: Input text that may contain LaTeX commands.

    Returns:
        LaTeX-safe string with commands preserved.
    """
    if text is None:
        return ""

    text = unicodedata.normalize("NFKC", text)

    # Remove invisible chars
    text = _remove_invisible_chars(text)

    # Replace dash-like characters with LaTeX-safe dash
    text = _normalize_dashes(text, replacement="{-}")

    # German sharp s (using a unique placeholder to avoid being escaped by _escape_special_chars)
    # Actually _escape_special_chars doesn't escape backslash or braces, so it's fine.
    text = text.replace("ß", r"{\ss}")

    # Escape LaTeX specials but don't touch backslashes/braces
    text = _escape_special_chars(text)

    return text

return_seite_page(lang)

Returns 'Seite' if German, 'page' if English.

Parameters:

Name Type Description Default
lang str

English or German

required

Returns:

Name Type Description
str str

"Seite" if German, "page" if English.

Source code in src/academic_doc_generator/core/latex.py
def return_seite_page(lang: str) -> str:
    """Returns 'Seite' if German, 'page' if English.

    Args:
        lang (str): English or German

    Returns:
        str: "Seite" if German, "page" if English.
    """
    return "Seite" if lang.lower().startswith("german") else "page"