import re from pathlib import Path from copienator import EvaluationWorkspace from .platform import validate_windows_labels def natural_key(text): return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', str(text))] def read_all_labels(base_dir): labels = EvaluationWorkspace(Path(base_dir)).read_labels() validate_windows_labels(labels) return labels def enonce_total(base_dir): text_dir = Path(base_dir) / 'Text' if not text_dir.is_dir(): return "" files = [f for f in text_dir.iterdir() if f.is_file() and f.suffix not in [".pdf", ".tex"]] files.sort(key=lambda f: natural_key(f.name)) output = [] for filepath in files: content = filepath.read_text(encoding='utf-8') output.append(f"{filepath.name}\n{content}\n\n\n") return "".join(output) import os import shlex from .platform import open_path def edit_file_and_enter(file): editor = os.environ.get("EDITOR") try: if editor: subprocess.run(shlex.split(editor, posix=os.name != "nt") + [str(file)]) else: open_path(file) input("Press ENTER here once you have saved and closed the text file...") except Exception as e: print(f"Error running editor: {e}") def pdf_image_of_enonce(root_dir, label): pdf_path = os.path.join(root_dir, "Text2", f"{label}.pdf") if os.path.exists(pdf_path): return pdf_path def pdf_image_of_solution(root_dir, label): pdf_path = os.path.join(root_dir, "Sol2", f"{label}.pdf") if os.path.exists(pdf_path): return pdf_path def pdf_images_of_contexts(root_dir, label, all_labels): text2_dir = os.path.join(root_dir, "Text2") if not os.path.isdir(text2_dir): return [] # Map safe labels (with '/' replaced by '_') to their chronological index safe_to_idx = {l.replace("/", "_"): i for i, l in enumerate(all_labels)} safe_target = label.replace("/", "_") target_idx = safe_to_idx.get(safe_target, -1) if target_idx == -1: return [] pertinent_contexts = [] for filename in sorted(os.listdir(text2_dir)): if filename.startswith("CTXT ") and filename.endswith(".pdf"): # Extract "first_label -> last_label" from "CTXT first_label -> last_label.pdf" core = filename[5:-4] parts = core.split(" -> ") if len(parts) == 2: first_safe, last_safe = parts first_idx = safe_to_idx.get(first_safe, -1) last_idx = safe_to_idx.get(last_safe, -1) # Check if the current label falls within the context's validity range if first_idx != -1 and last_idx != -1: if first_idx <= target_idx <= last_idx: pdf_path = os.path.join(text2_dir, filename) pertinent_contexts.append((first_idx, pdf_path)) # Sort by first_idx to ensure contexts are returned in logical reading order pertinent_contexts.sort(key=lambda x: x[0]) return [path for _, path in pertinent_contexts] def get_exam_file_content(folder_path, mode, label): """Use the most specific exact, grouped, or enclosing section available.""" target_dir = Path(folder_path) / mode if not target_dir.is_dir(): return None # Sanitize label (consistent with the script's saving logic) safe_label = label.replace("/", "_") # 1. Try exact filename match direct_file = target_dir / safe_label if direct_file.is_file(): return direct_file.read_text(encoding="utf-8") candidates = [] for file_path in sorted(target_dir.iterdir()): if not file_path.is_file(): continue name = file_path.name if file_path.suffix in {".pdf", ".tex"}: continue covered_labels = [name] if "[" in name and name.endswith("]"): prefix, rest = name.split("[", 1) covered_labels = [prefix + suffix.strip() for suffix in rest[:-1].split(",")] for covered in covered_labels: if covered == safe_label or _is_parent_label(covered, safe_label): candidates.append((len(covered), name == covered, file_path)) if candidates: # Prefer a narrower section, then its direct file over a grouped file. candidates.sort(key=lambda item: (-item[0], -item[1], item[2].name)) return candidates[0][2].read_text(encoding="utf-8") return None def _is_parent_label(parent, label): if not parent or not label.startswith(parent): return False suffix = label[len(parent):] # Respect component boundaries: Ex 1 must not match Ex 10, nor 1) match 10). return bool( suffix.startswith((" ", ":", ".")) or (parent.endswith(")") and re.match(r"[A-Za-z0-9]+\)", suffix)) ) def get_label_text_content(folder, label): return get_exam_file_content(folder, "Text", label) def get_label_sol_content(folder, label): return get_exam_file_content(folder, "Sol", label) def get_label_persp_content(folder, label): return get_exam_file_content(folder, "Persp", label) def warn_missing_exam_material(workspace): """Report prompt coverage after statement preparation without blocking work.""" if not workspace.labels_file.is_file(): print("[Vérification des prompts] Avertissement : fichier labels absent ; vérification impossible.") return labels = workspace.read_labels() missing_count = 0 for label in labels: missing = [] for mode, title in (("Text", "énoncé"), ("Sol", "corrigé")): try: content = get_exam_file_content(workspace.root, mode, label) if not content or not content.strip() or content.strip().rstrip(" :") == label: missing.append(f"{title} absent ou vide ({mode})") except (OSError, UnicodeError) as exc: missing.append(f"{title} illisible ({mode}) : {exc}") if missing: missing_count += 1 print(f"[Vérification des prompts] Avertissement — {label} : {' ; '.join(missing)}") print( f"[Vérification des prompts] {len(labels) - missing_count}/{len(labels)} labels " "avec énoncé et corrigé non vides." ) if missing_count: print("[Vérification des prompts] Vous pouvez continuer ; les corrections des labels signalés utiliseront des documents incomplets.") import tempfile import shutil import subprocess from .configuration import LATEX_AFTER, LATEX_BEFORE LATEX_WARNING_MARKER = "[Copienator:latex-warning] " def report_latex_warning(output_pdf_path) -> None: """Emit a stable, machine-readable path alongside human diagnostics.""" print(f"{LATEX_WARNING_MARKER}{Path(output_pdf_path).resolve()}") def fragment_latex_header(source_path=None): """Keep source packages/macros while using the configured fragment layout.""" if source_path is None: return LATEX_BEFORE source = Path(source_path).read_text(encoding="utf-8") # Ignore commented document commands when locating the preamble. source = re.sub(r"(?m)(?