Évaluation 2 — Sujet
2025-2026 · tout le cours
Informations générales
- Durée : 3 heures
- Note : /20
- Documents autorisés : le site web du cours uniquement — https://remisharrock.gitlab.io/typescript101/
- Internet : non disponible (mode examen hors ligne)
- Intelligence artificielle : interdite
| Partie | Contenu | Points |
|---|---|---|
| Partie 1 | Questions théoriques (5 questions) | 10 points |
| Partie 2 | Exercice de codage — Gestionnaire de Playlist | 10 points |
| Total | 20 points |
Mise en place (5 minutes)
Téléchargez l’archive outillage.tar.gz depuis la page Outillage et décompressez-la.
- Copiez le dossier
outillage/projet_templateen entier, renommez la copiemon_projet(à la souris, ou aveccp -R outillage/projet_template mon_projet—-Ren majuscule). - Dans un terminal :
cd mon_projetpuisnpx astro dev - Ouvrez
http://localhost:4321/dans le navigateur. - Vos fichiers
.astrovont danssrc/pages/, vos fichiers.tsdanssrc/scripts/.
Le détail complet et la résolution de problèmes se trouvent dans outillage/README.html.
Partie 1 — Questions théoriques
Durée indicative : 1h30 — 5 questions, 10 points
Répondez dans le fichier reponse_NOM.txt, entre les balises %%%DEBUT_Q1%%% … %%%FIN_Q1%%%, etc. Répondez à chaque question de manière claire et argumentée.
Question 1 — JSON, sérialisation et limites de la désérialisation (2 points)
Considérez le code TypeScript suivant :
interface IAnimal {
nom: string;
age: number;
crier(): string;
}
class Chat implements IAnimal {
nom: string;
age: number;
constructor(nom: string, age: number) {
this.nom = nom;
this.age = age;
}
crier(): string {
return "Miaou !";
}
ronronner(): string {
return "Rrrr...";
}
}
const monChat = new Chat("Felix", 3);
const json: string = JSON.stringify(monChat);
console.log(json);
const chatRestaure: IAnimal = JSON.parse(json);
console.log(chatRestaure.nom);
console.log(chatRestaure.crier());a) Que retourne JSON.stringify(monChat) (quelle chaîne est affichée par le console.log) ? Quelles propriétés sont présentes dans la chaîne JSON produite et qu’est-ce qui n’y figure pas ? Expliquez pourquoi.
b) La ligne chatRestaure.crier() compile sans erreur. Pourtant, que se passe-t-il à l’exécution ? Expliquez pourquoi TypeScript ne détecte pas ce problème. Dans un projet réel qui reçoit des données d’une API distante via fetch, pourquoi cette limitation est-elle particulièrement dangereuse ?
c) Écrivez une fonction toChat(data: IAnimal): Chat qui crée une véritable instance de Chat à partir des données désérialisées, permettant d’appeler crier() et ronronner().
Question 2 — Génériques (1,5 points)
Considérez le code suivant :
class Boite<T> {
private contenu: T[];
constructor() {
this.contenu = [];
}
ajouter(element: T): void {
this.contenu.push(element);
}
retirer(): T | undefined {
return this.contenu.pop();
}
}
const boiteNombres = new Boite<number>();
boiteNombres.ajouter(42);
boiteNombres.ajouter(17);
boiteNombres.ajouter("test");
const valeur = boiteNombres.retirer();a) Quelle erreur produit boiteNombres.ajouter("test") ? Expliquez comment les génériques assurent la sécurité des types. Quel est le type exact de valeur et pourquoi inclut-il undefined ?
b) Un collègue propose d’utiliser any[] au lieu de T[] pour « simplifier ». Sans réécrire toute la classe, donnez un exemple concret de deux lignes de code (un appel à ajouter puis une utilisation du résultat de retirer()) qui compilerait sans erreur avec any mais planterait à l’exécution. Expliquez en une phrase pourquoi les génériques empêchent ce scénario.
c) Si on crée const boiteMixte = new Boite<number | string>(), peut-on y ajouter à la fois des nombres et des chaînes ? Quel est alors le type de retour de retirer() et quelle vérification faudrait-il faire avant d’utiliser la valeur retournée ?
Question 3 — Exceptions et gestion d’erreurs (1,5 points)
Considérez le code suivant :
class ErreurAge extends Error {
age: number;
constructor(age: number) {
super(`Âge invalide : ${age}`);
this.name = "ErreurAge";
this.age = age;
}
}
class ErreurNom extends Error {
constructor(nom: string) {
super(`Nom invalide : "${nom}"`);
this.name = "ErreurNom";
}
}
function creerUtilisateur(nom: string, age: number): { nom: string; age: number } {
if (nom.length < 2) {
throw new ErreurNom(nom);
}
if (age < 0 || age > 150) {
throw new ErreurAge(age);
}
return { nom, age };
}
try {
const u1 = creerUtilisateur("Alice", 25);
console.log("A:", u1.nom);
const u2 = creerUtilisateur("B", 30);
console.log("B:", u2.nom);
const u3 = creerUtilisateur("Charlie", -5);
console.log("C:", u3.nom);
} catch (e: unknown) {
if (e instanceof ErreurNom) {
console.log("Erreur nom:", e.message);
} else if (e instanceof ErreurAge) {
console.log("Erreur age:", e.message, e.age);
} else if (e instanceof Error) {
console.log("Erreur:", e.message);
}
} finally {
console.log("Fin");
}a) Tracez l’exécution : quelles lignes sont affichées dans la console, et dans quel ordre ? Pourquoi la création de u3 n’est-elle jamais atteinte ?
b) Pourquoi l’ordre des if / else if est-il important dans le bloc catch ? Que se passerait-il si on testait e instanceof Error en premier ? (Indice : pensez à l’héritage.)
c) Un collègue modifie le code pour mettre chaque appel à creerUtilisateur dans son propre bloc try/catch séparé. Quel avantage concret cela apporte-t-il par rapport au bloc try unique englobant les trois appels ?
Question 4 — Async/Await et Promesses (2,5 points)
Considérez le code suivant :
function attendre(ms: number): Promise<string> {
return new Promise((resolve, reject) => {
if (ms < 0) {
reject(new Error("Durée négative"));
}
setTimeout(() => {
resolve(`Terminé après ${ms}ms`);
}, ms);
});
}
async function scenario() {
console.log("1. Début");
const resultat = await attendre(1000);
console.log("2.", resultat);
try {
const erreur = await attendre(-1);
console.log("3.", erreur);
} catch (e) {
if (e instanceof Error) {
console.log("4.", e.message);
}
}
console.log("5. Fin");
}
console.log("A. Avant");
scenario();
console.log("B. Après");a) Dans quel ordre apparaissent les messages dans la console ? Expliquez pourquoi "B. Après" s’affiche avant "2." alors qu’il apparaît après l’appel à scenario() dans le code.
b) Quel est le type de retour de attendre(1000) ? Quel est le type de resultat après le await ? Expliquez la transformation que await opère sur une promesse.
c) Si on remplaçait scenario(); par await scenario(); (en enveloppant le code de niveau supérieur dans une fonction async), comment l’ordre d’affichage changerait-il ? Justifiez.
d) Réécrivez les deux lignes const resultat = await attendre(1000); et console.log("2.", resultat); en version équivalente utilisant .then() au lieu de await.
Question 5 — CORS, Web Components et Formulaires (2,5 points)
Partie A — CORS (1 point)
Un étudiant développe une application Astro. Dans le frontmatter (---), il fait await fetch("https://api.exemple.com/data") et tout fonctionne. Il copie la même ligne dans un <script> côté client et obtient une erreur CORS dans la console du navigateur.
a) Expliquez pourquoi le même appel fetch fonctionne à un endroit mais pas à l’autre. Où s’exécute chacun des deux codes ?
b) Proposez deux solutions pour que le client puisse quand même accéder aux données. L’une de ces solutions doit utiliser la technique data-* + JSON.stringify vue en cours.
Partie B — Web Components et Formulaires (1,5 points)
Considérez le Web Component suivant :
class AlerteMessage extends HTMLElement {
static get observedAttributes(): string[] {
return ["message"];
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
this.render();
}
render() {
const msg = this.getAttribute("message") || "Aucun message";
this.innerHTML = `<div style="border:2px solid red; padding:10px;">${msg}</div>`;
}
}
customElements.define("alerte-message", AlerteMessage);Et ce formulaire :
const form = document.getElementById("alertForm") as HTMLFormElement;
const input = document.getElementById("msgInput") as HTMLInputElement;
const alerte = document.querySelector("alerte-message") as AlerteMessage;
form.addEventListener("submit", function(event: Event) {
alerte.setAttribute("message", input.value);
console.log(this);
});c) Quand le formulaire est soumis, l’alerte clignote brièvement puis la page se recharge et revient à son état initial. Identifiez le problème et proposez la correction (une seule ligne à ajouter, et à quel endroit).
d) Dans le gestionnaire submit, console.log(this) affiche l’élément <form>, pas l’objet global. Si on remplace function(event: Event) { ... } par une fonction fléchée (event: Event) => { ... }, que changerait la valeur de this et pourquoi ?
Fin de la Partie 1
Partie 2 — Exercice de codage
Durée indicative : 1h30 — 10 points
Contexte
Vous travaillez sur un gestionnaire de playlist musicale. On vous fournit un catalogue de pistes musicales sous forme de chaîne JSON dans un module TypeScript. Votre mission est de créer une page web interactive qui parse ces données, les affiche, utilise un Web Component personnalisé, et propose un formulaire de recherche.
Travaillez dans votre projet mon_projet pour écrire et tester votre code, puis recopiez le code de chaque section dans le fichier reponse_NOM.txt, entre les balises %%%DEBUT_S1%%%, %%%DEBUT_S2%%% et %%%DEBUT_S3%%%.
Rendez toujours ce que vous avez écrit, même incomplet. Les trois sections sont largement indépendantes les unes des autres.
Organisation des fichiers dans votre projet brouillon
mon_projet/
└── src/
├── pages/
│ └── index.astro ← à remplacer par le fichier fourni ci-dessous
└── scripts/
├── catalogueMusique.ts ← à créer avec le contenu fourni ci-dessous
└── app.ts ← à remplacer par le squelette fourni ci-dessous
Aide-mémoire
Voici quelques méthodes et rappels utiles pour cet exercice :
.join(separateur) — Concaténer un tableau en une chaîne
La méthode .join() transforme un tableau en une seule chaîne de caractères, en insérant un séparateur entre chaque élément :
const fruits: string[] = ["pomme", "banane", "orange"];
console.log(fruits.join(", ")); // "pomme, banane, orange".slice(debut, fin) — Extraire une portion de tableau
La méthode .slice() retourne un nouveau tableau contenant les éléments de l’index debut jusqu’à l’index fin (non inclus) :
const nombres: number[] = [10, 20, 30, 40, 50];
const premiers: number[] = nombres.slice(0, 3); // [10, 20, 30]JSON.parse() et JSON.stringify()
// Désérialisation : chaîne JSON → objet JavaScript
const data: MonType[] = JSON.parse(chaineJSON);
// Sérialisation : objet JavaScript → chaîne JSON
const json: string = JSON.stringify(monObjet);Créer un Web Component minimal
class MonElement extends HTMLElement {
static get observedAttributes(): string[] {
return ["mon-attribut"];
}
connectedCallback() { this.render(); }
attributeChangedCallback() { this.render(); }
render() {
this.innerHTML = `<p>${this.getAttribute("mon-attribut")}</p>`;
}
}
customElements.define("mon-element", MonElement);event.preventDefault() — Empêcher le comportement par défaut
form.addEventListener("submit", (event: Event) => {
event.preventDefault(); // empêche le rechargement de la page
// ... traitement du formulaire
});Fichier fourni 1 — src/scripts/catalogueMusique.ts
Créez ce fichier et recopiez-y ce contenu sans le modifier.
export interface Piste {
titre: string;
artiste: string;
album: string;
duree: number; // en secondes
genre: string;
note: number; // entre 0 et 5
}
// Données fournies sous forme de chaîne JSON (simulant une réponse d'API)
export const pistesJSON: string = `[
{"titre":"Bohemian Rhapsody","artiste":"Queen","album":"A Night at the Opera","duree":354,"genre":"Rock","note":5},
{"titre":"Imagine","artiste":"John Lennon","album":"Imagine","duree":187,"genre":"Pop","note":4.5},
{"titre":"Billie Jean","artiste":"Michael Jackson","album":"Thriller","duree":294,"genre":"Pop","note":4.8},
{"titre":"Smells Like Teen Spirit","artiste":"Nirvana","album":"Nevermind","duree":301,"genre":"Rock","note":4.7},
{"titre":"Like a Rolling Stone","artiste":"Bob Dylan","album":"Highway 61 Revisited","duree":369,"genre":"Folk","note":4.3},
{"titre":"One","artiste":"U2","album":"Achtung Baby","duree":276,"genre":"Rock","note":4.1},
{"titre":"Lose Yourself","artiste":"Eminem","album":"8 Mile","duree":326,"genre":"Rap","note":4.6},
{"titre":"Hey Jude","artiste":"The Beatles","album":"Single","duree":431,"genre":"Pop","note":4.9},
{"titre":"Redemption Song","artiste":"Bob Marley","album":"Uprising","duree":227,"genre":"Reggae","note":4.4},
{"titre":"Purple Rain","artiste":"Prince","album":"Purple Rain","duree":520,"genre":"Pop","note":4.7}
]`;
// Données invalides pour tester la gestion d'erreurs (JSON volontairement mal formé)
export const pistesJSON_invalide: string = `[
{"titre":"Test","artiste":"Artiste", duree: "pas un nombre"}
`;
export const formatDuree = (secondes: number): string => {
const min = Math.floor(secondes / 60);
const sec = secondes % 60;
return `${min}:${sec.toString().padStart(2, "0")}`;
};Fichier fourni 2 — src/pages/index.astro
Remplacez le contenu du fichier existant par celui-ci.
---
---
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Playlist WebDev101</title>
<style>
body {
font-family: sans-serif;
max-width: 900px;
margin: 20px auto;
padding: 0 20px;
}
h1 { color: #2c3e50; }
h2 { color: #34495e; border-bottom: 2px solid #e74c3c; padding-bottom: 5px; }
.resultat {
background-color: #f9f9f9;
border: 1px solid #ddd;
padding: 10px;
margin: 10px 0;
border-radius: 5px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #e74c3c;
color: white;
}
tr:nth-child(even) { background-color: #f2f2f2; }
.top { color: gold; font-weight: bold; }
.normal { color: #333; }
.erreur { color: red; font-style: italic; }
piste-card {
display: block;
border: 1px solid #ccc;
border-radius: 8px;
padding: 10px;
margin: 5px 0;
background: #fafafa;
}
form { margin: 10px 0; }
form label { margin-right: 10px; }
form input[type="text"] { padding: 5px; margin-right: 10px; }
form button {
padding: 5px 15px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>🎵 Playlist WebDev101</h1>
<h2>1. Catalogue (JSON → tableau)</h2>
<div id="catalogue" class="resultat"></div>
<h2>2. Web Component <piste-card></h2>
<div id="composants" class="resultat"></div>
<h2>3. Recherche et statistiques</h2>
<form id="searchForm">
<label for="searchInput">Artiste :</label>
<input type="text" id="searchInput" placeholder="Nom de l'artiste..." />
<button type="submit">Rechercher</button>
</form>
<div id="recherche" class="resultat"></div>
<div id="statistiques" class="resultat"></div>
<script src="../scripts/app.ts"></script>
</body>
</html>Fichier fourni 3 — src/scripts/app.ts (squelette à compléter)
C’est ce fichier que vous devez compléter. Remplacez le contenu du fichier existant par ce squelette. Chaque section est repérée par un commentaire // TODO.
// TODO 1 : Importez depuis le module catalogueMusique.ts :
// - l'interface Piste
// - la chaîne pistesJSON
// - la chaîne pistesJSON_invalide
// - la fonction formatDuree
// ============================================
// SECTION 1 : Désérialisation JSON et affichage (3,5 points)
// ============================================
// TODO 2 : Créez une fonction fléchée "parsePistes" qui prend une chaîne JSON
// en paramètre et retourne un tableau de Piste.
// Cette fonction doit utiliser try/catch :
// - dans le try : parser le JSON avec JSON.parse() et retourner le résultat
// - dans le catch : afficher un message d'erreur dans la console et retourner un tableau vide
//
// Testez d'abord avec pistesJSON_invalide pour vérifier que l'erreur est bien gérée,
// puis parsez pistesJSON pour obtenir le tableau de pistes.
// TODO 3 : Créez une fonction fléchée "creerLigneTableau" qui prend une Piste
// en paramètre et retourne une chaîne HTML représentant une ligne de tableau <tr>
// avec les colonnes : titre, artiste, album, durée (formatée avec formatDuree), genre, note.
// La ligne doit avoir la classe CSS "top" si la note >= 4.7, sinon "normal".
//
// Utilisez map avec creerLigneTableau pour transformer le tableau de pistes
// en lignes HTML, puis affichez le résultat dans le div #catalogue sous forme
// de tableau HTML complet avec les en-têtes.
// Rappel : utilisez .join("") pour concaténer les lignes du tableau.
const catalogueDiv = document.getElementById("catalogue") as HTMLDivElement;
// ============================================
// SECTION 2 : Web Component <piste-card> (3 points)
// ============================================
// TODO 4 : Créez un Web Component "piste-card" (classe PisteCard qui étend HTMLElement).
// Ce composant doit :
// - observer les attributs "titre" et "artiste" (via observedAttributes)
// - implémenter connectedCallback() qui appelle render()
// - implémenter attributeChangedCallback() qui appelle render()
// - implémenter render() qui affiche :
// <strong>TITRE</strong> — <em>ARTISTE</em>
// (en utilisant getAttribute("titre") et getAttribute("artiste"))
//
// Enregistrez le composant avec customElements.define("piste-card", PisteCard).
//
// Puis utilisez forEach sur les 5 premières pistes (utilisez .slice(0, 5))
// pour créer dynamiquement des éléments <piste-card> et les ajouter au div #composants.
// Pour chaque piste, créez l'élément avec document.createElement("piste-card"),
// définissez les attributs "titre" et "artiste" avec setAttribute(), puis ajoutez-le au div.
const composantsDiv = document.getElementById("composants") as HTMLDivElement;
// ============================================
// SECTION 3 : Recherche par artiste et durée totale (3,5 points)
// ============================================
// TODO 5 : Récupérez le formulaire #searchForm et l'input #searchInput.
// Ajoutez un gestionnaire d'événement "submit" sur le formulaire.
// N'oubliez pas d'appeler event.preventDefault() pour empêcher le rechargement de la page !
//
// Dans le gestionnaire :
// - récupérez la valeur saisie dans l'input
// - utilisez "filter" pour trouver toutes les pistes dont le nom de l'artiste
// contient la valeur recherchée (insensible à la casse, utilisez .toLowerCase())
// - si aucune piste trouvée, affichez "Aucune piste trouvée pour cet artiste"
// avec la classe CSS "erreur"
// - sinon, affichez les résultats sous forme de liste <ul><li>
// Chaque puce : "Titre — Artiste (Album, durée)"
const rechercheDiv = document.getElementById("recherche") as HTMLDivElement;
// TODO 6 : Calculez la durée totale de toutes les pistes avec "reduce".
// Affichez le résultat dans le div #statistiques, formaté avec formatDuree :
// "Durée totale de la playlist : X:XX"
const statsDiv = document.getElementById("statistiques") as HTMLDivElement;Travail demandé — détail des sections
Section 1 — Désérialisation JSON et affichage du catalogue (3,5 points) → %%%DEBUT_S1%%%
- Importez depuis le module
catalogueMusique.ts: l’interfacePiste, les chaînespistesJSONetpistesJSON_invalide, et la fonctionformatDuree - Créez une fonction fléchée
parsePistesqui prend une chaîne JSON en paramètre et retourne un tableau dePiste. Utiliseztry/catch: dans letry, parsez le JSON et retournez le résultat ; dans lecatch, affichez l’erreur dans la console et retournez un tableau vide - Testez d’abord avec
pistesJSON_invalidepour vérifier la gestion d’erreurs, puis parsezpistesJSON - Créez une fonction fléchée
creerLigneTableauqui prend unePisteet retourne une chaîne HTML<tr>avec les colonnes : titre, artiste, album, durée (formatée avecformatDuree), genre, note. La ligne doit avoir la classe CSS"top"si la note ≥ 4.7, sinon"normal" - Utilisez
mappour transformer le tableau de pistes en lignes HTML et affichez dans lediv#cataloguesous forme de tableau complet
Recopiez la ligne d’import dans la balise %%%DEBUT_S1%%% : elle fait partie de cette section.
Section 2 — Web Component <piste-card> (3 points) → %%%DEBUT_S2%%%
- Créez un Web Component
PisteCard(classe héritant deHTMLElement) qui observe les attributs"titre"et"artiste"(viaobservedAttributes) - Implémentez
connectedCallback()etattributeChangedCallback()qui appellentrender() render()affiche :<strong>TITRE</strong> — <em>ARTISTE</em>(en utilisantgetAttribute)- Enregistrez le composant avec
customElements.define("piste-card", PisteCard) - Utilisez
forEachsur les 5 premières pistes (.slice(0, 5)) pour créer dynamiquement des éléments<piste-card>avecdocument.createElement, définissez les attributs et ajoutez-les audiv#composants
Section 3 — Recherche par artiste et durée totale (3,5 points) → %%%DEBUT_S3%%%
- Récupérez le formulaire
#searchFormet l’input#searchInput - Ajoutez un gestionnaire d’événement
submitavecevent.preventDefault() - Utilisez
filterpour trouver les pistes dont l’artiste contient la valeur recherchée (insensible à la casse avec.toLowerCase()) - Si aucun résultat, affichez
"Aucune piste trouvée pour cet artiste"avec la classe CSS"erreur" - Sinon, affichez les résultats sous forme de liste
<ul><li>:"Titre — Artiste (Album, durée)" - Calculez la durée totale de toutes les pistes avec
reduceet affichez-la dans lediv#statistiquesformatée avecformatDuree
Fin de la Partie 2
Rappel du fichier de réponses
Le fichier reponse_NOM.txt fourni dans l’outillage a la structure suivante. En cas de perte, vous pouvez le recréer à l’identique.
%%%DEBUT_IDENTITE%%%
NOM :
PRENOM :
%%%FIN_IDENTITE%%%
%%%DEBUT_Q1%%%
a)
b)
c)
%%%FIN_Q1%%%
%%%DEBUT_Q2%%%
...
%%%FIN_Q2%%%
%%%DEBUT_Q3%%%
...
%%%FIN_Q3%%%
%%%DEBUT_Q4%%%
...
%%%FIN_Q4%%%
%%%DEBUT_Q5%%%
...
%%%FIN_Q5%%%
%%%DEBUT_S1%%%
(code de la section 1)
%%%FIN_S1%%%
%%%DEBUT_S2%%%
(code de la section 2)
%%%FIN_S2%%%
%%%DEBUT_S3%%%
(code de la section 3)
%%%FIN_S3%%%
%%%DEBUT_REMARQUES%%%
(facultatif)
%%%FIN_REMARQUES%%%
Bon courage !
Comment rendre votre copie
Un seul fichier
Vous rendez un seul fichier texte : le fichier
reponse_NOM.txtfourni dans l’outillage.outillage/reponse_NOM.txtdans votre éditeur.reponse_DUPONT.txt.Les balises
Le fichier contient des balises de ce type :
Ne modifiez pas, ne déplacez pas et ne supprimez pas les balises. Elles servent à retrouver chacune de vos réponses. Une balise abîmée ou manquante rend la réponse introuvable, et donc non notée.
Les balises attendues pour cette évaluation sont :
%%%DEBUT_IDENTITE%%%…%%%FIN_IDENTITE%%%%%%DEBUT_Q1%%%…%%%FIN_Q1%%%%%%DEBUT_Q2%%%…%%%FIN_Q2%%%%%%DEBUT_Q3%%%…%%%FIN_Q3%%%%%%DEBUT_Q4%%%…%%%FIN_Q4%%%%%%DEBUT_Q5%%%…%%%FIN_Q5%%%%%%DEBUT_S1%%%…%%%FIN_S1%%%%%%DEBUT_S2%%%…%%%FIN_S2%%%%%%DEBUT_S3%%%…%%%FIN_S3%%%%%%DEBUT_REMARQUES%%%…%%%FIN_REMARQUES%%%Votre responsabilité
Ctrl+S). Des copies ont déjà été perdues à quelques minutes de la fin.