TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1import { describe, expect, it } from "vitest";2import { detexify, parseArticleSections, parseBeamerFrames, splitLong, stripComments } from "../lib/rag/latex.ts";34const SAMPLE_BEAMER = String.raw`5\documentclass{beamer}6\begin{document}7\begin{frame}[plain,noframenumbering]8\titlepage9\end{frame}10\begin{frame}{Plan de la séance}11\tableofcontents12\end{frame}13\section{Première section}14\begin{frame}{Titre A}15\begin{defbox}[Valeur marchande]16Le prix le plus probable. % commentaire17\end{defbox}18\begin{itemize}19 \item Premier \textbf{point} important20 \item Deuxième point21\end{itemize}22\end{frame}23\begin{frame}{Titre B (1/2)}24Contenu avec formule $V = \frac{RNE}{TGA}$ et tableau :25\begin{tabular}{ll}26\toprule27Poste & Montant \\28Revenus & 100~000 \\29\bottomrule30\end{tabular}31\end{frame}32\end{document}`;3334describe("Parseur beamer", () => {35 const frames = parseBeamerFrames(SAMPLE_BEAMER);3637 it("numérote les diapositives comme le PDF (page titre non numérotée)", () => {38 // Plan = diapositive 1, Titre A = 2, Titre B = 339 const a = frames.find((f) => f.title === "Titre A");40 expect(a?.slideNumber).toBe(2);41 });4243 it("capture la section courante et les boîtes sémantiques", () => {44 const a = frames.find((f) => f.title === "Titre A")!;45 expect(a.sectionTitle).toBe("Première section");46 expect(a.boxTypes).toContain("defbox");47 expect(a.content).toContain("Définition — Valeur marchande");48 expect(a.content).toContain("prix le plus probable");49 expect(a.content).not.toContain("commentaire");50 });5152 it("aplatit les listes et conserve les maths", () => {53 const a = frames.find((f) => f.title === "Titre A")!;54 expect(a.content).toContain("• Premier point important");55 const b = frames.find((f) => f.title.startsWith("Titre B"))!;56 expect(b.content).toContain("$V =");57 expect(b.content).toContain("Revenus | 100 000");58 });59});6061describe("detexify", () => {62 it("convertit les guillemets français et symboles", () => {63 expect(detexify(String.raw`\og valeur \fg{} \rightarrow 15~\%`)).toBe("« valeur » → 15 %");64 expect(detexify(String.raw`a \rightarrow b`)).toContain("→");65 });66 it("supprime les commandes inconnues sans perdre le texte", () => {67 expect(detexify(String.raw`\foobar{} Texte \textcolor{red}{rouge} conservé`)).toContain("Texte rouge conservé");68 });69});7071describe("parseArticleSections", () => {72 it("découpe par sections avec chemin hiérarchique", () => {73 const tex = String.raw`\begin{document}74\section{Grande section}75Texte A suffisant pour être conservé dans la sortie finale.76\subsection{Sous-partie}77Texte B suffisant pour être conservé dans la sortie finale.78\end{document}`;79 const sections = parseArticleSections(tex);80 expect(sections).toHaveLength(2);81 expect(sections[1].path).toBe("Grande section > Sous-partie");82 });83});8485describe("splitLong", () => {86 it("respecte les paragraphes et garde un chevauchement", () => {87 const text = Array.from({ length: 30 }, (_, i) => `Paragraphe ${i} avec du contenu assez long pour compter.`).join("\n\n");88 const parts = splitLong(text, 500);89 expect(parts.length).toBeGreaterThan(2);90 for (const p of parts) expect(p.length).toBeLessThan(700);91 });92});9394describe("stripComments", () => {95 it("préserve les pourcentages échappés", () => {96 expect(stripComments("15\\% du prix % vrai commentaire")).toBe("15\\% du prix ");97 });98});99