/* ===================================================================== script.js — le "moteur" de la page --------------------------------------------------------------------- Ce fichier fait fonctionner les éditeurs de code interactifs. Il est volontairement TRÈS commenté : c'est lui-même un exemple de vrai code JavaScript. Une fois que tu auras lu les chapitres, reviens le lire : tu devrais tout comprendre ! Ce qu'il fait : 1. Trouver tous les éditeurs (.bac) de la page 2. Quand on clique « Exécuter », lancer le code écrit dedans 3. Intercepter les console.log pour les afficher DANS la page 4. Traduire les erreurs en français pour aider à comprendre 5. Surligner dans le menu le chapitre en cours de lecture 6. Gérer le bouton clair / sombre ===================================================================== */ /* --------------------------------------------------------------------- "use strict" active le MODE STRICT : JavaScript devient plus sévère et signale des erreurs qu'il ignorerait sinon (ex : utiliser une variable jamais déclarée). C'est une bonne habitude. --------------------------------------------------------------------- */ "use strict"; /* ===================================================================== 1. OUTIL : transformer n'importe quelle valeur en texte lisible ===================================================================== console.log("bonjour") est facile à afficher. Mais comment afficher un tableau [1, 2, 3] ou un objet { nom: "Ada" } ? Cette fonction regarde le TYPE de la valeur et fabrique un texte adapté. Elle renvoie un élément HTML coloré selon le type. --------------------------------------------------------------------- */ function formaterValeur(valeur, profondeur = 0) { // typeof renvoie le type de la valeur sous forme de texte : // "string", "number", "boolean", "undefined", "object", "function"… const type = typeof valeur; // Un "texte" (string) : au premier niveau on l'affiche tel quel, // à l'intérieur d'un tableau/objet on l'entoure de guillemets. if (type === "string") { return profondeur === 0 ? valeur : `"${valeur}"`; } if (type === "number" || type === "boolean" || type === "bigint") { return String(valeur); // String(42) → "42" } if (valeur === undefined) return "undefined"; if (valeur === null) return "null"; if (type === "function") { // valeur.name = le nom de la fonction (vide si anonyme) return `ƒ ${valeur.name || "(anonyme)"}()`; } // On évite de descendre trop profond (objets imbriqués à l'infini) if (profondeur > 3) return "…"; // Un tableau : Array.isArray est LA façon fiable de le reconnaître, // car typeof [] renvoie "object" (une bizarrerie historique). if (Array.isArray(valeur)) { // .map transforme chaque élément, .join colle le tout avec ", " const morceaux = valeur.map(v => formaterValeur(v, profondeur + 1)); return `[${morceaux.join(", ")}]`; } // Un élément HTML (ex : document.querySelector("h1")) if (valeur instanceof Element) { const classes = valeur.className ? "." + String(valeur.className).split(" ").join(".") : ""; return `<${valeur.tagName.toLowerCase()}${valeur.id ? "#" + valeur.id : ""}${classes}>`; } // Une Promise (chapitre asynchrone) if (valeur instanceof Promise) return "Promise { … }"; // Une erreur if (valeur instanceof Error) return `${valeur.name}: ${valeur.message}`; // Un objet "classique" : { cle: valeur, ... } if (type === "object") { // Le nom du "constructeur" : utile pour les objets créés avec une classe const nomClasse = valeur.constructor && valeur.constructor.name !== "Object" ? valeur.constructor.name + " " : ""; // Object.entries({a:1, b:2}) → [["a",1], ["b",2]] const paires = Object.entries(valeur).map( ([cle, v]) => `${cle}: ${formaterValeur(v, profondeur + 1)}` ); return `${nomClasse}{ ${paires.join(", ")} }`; } return String(valeur); } /* Renvoie le nom d'une classe CSS selon le type, pour colorer la sortie */ function classeCouleur(valeur) { if (typeof valeur === "number") return "t-nombre"; if (typeof valeur === "string") return "t-texte"; if (typeof valeur === "boolean") return "t-booleen"; if (valeur === null || valeur === undefined) return "t-vide"; return ""; } /* ===================================================================== 2. OUTIL : traduire les erreurs JavaScript en français ===================================================================== Quand le code plante, JavaScript lance une "erreur" avec un nom (ReferenceError, TypeError…) et un message en anglais. On ajoute une petite explication pour débutant. --------------------------------------------------------------------- */ const EXPLICATIONS_ERREURS = { // Un objet utilisé comme un "dictionnaire" : nom de l'erreur → explication ReferenceError: "Tu utilises un nom (variable ou fonction) qui n'existe pas. Faute de frappe ? Variable déclarée plus bas ou dans un autre bloc { } ?", TypeError: "Tu fais une opération impossible sur ce type de valeur (ex : appeler quelque chose qui n'est pas une fonction, lire une propriété de undefined, modifier une const).", SyntaxError: "Le code est mal écrit : il manque sans doute une parenthèse, une accolade, un guillemet ou une virgule.", RangeError: "Une valeur est hors des limites autorisées (souvent : une fonction qui s'appelle elle-même à l'infini).", }; function expliquerErreur(erreur) { // L'opérateur ?? ("coalescence des nuls") donne la valeur de droite // si celle de gauche est null ou undefined. return EXPLICATIONS_ERREURS[erreur.name] ?? ""; } /* ===================================================================== 3. UNE "FAUSSE" CONSOLE pour chaque éditeur ===================================================================== Le vrai console.log écrit dans les outils de développement (F12). On fabrique un objet qui a les MÊMES méthodes (log, warn, error…) mais qui écrit dans la zone .sortie de l'éditeur. C'est une "fabrique" : une fonction qui renvoie un objet. --------------------------------------------------------------------- */ function creerConsole(zoneSortie) { // Fonction interne qui ajoute UNE ligne dans la zone de sortie. // "...valeurs" (paramètre du reste) récupère TOUS les arguments // dans un tableau : console.log("a", 1, true) → ["a", 1, true] function ecrire(niveau, ...valeurs) { const ligne = document.createElement("div"); // crée un
vide ligne.className = "ligne " + niveau; // Pour chaque valeur, on crée un coloré, séparés par un espace valeurs.forEach((valeur, index) => { if (index > 0) ligne.append(" "); const span = document.createElement("span"); span.className = classeCouleur(valeur); // textContent (et pas innerHTML) : le texte est affiché tel quel, // une balise s'affichera littéralement au lieu d'être interprétée. span.textContent = formaterValeur(valeur); ligne.append(span); }); zoneSortie.append(ligne); // ajoute dans la page zoneSortie.scrollTop = zoneSortie.scrollHeight; // défile tout en bas // On envoie AUSSI dans la vraie console du navigateur (F12) console[niveau === "err" ? "error" : niveau === "warn" ? "warn" : "log"](...valeurs); } // L'objet renvoyé : mêmes noms de méthodes que la vraie console return { log: (...v) => ecrire("", ...v), info: (...v) => ecrire("info", ...v), warn: (...v) => ecrire("warn", "⚠", ...v), error: (...v) => ecrire("err", "✖", ...v), clear: () => { zoneSortie.innerHTML = ""; }, // console.table : affiche un tableau d'objets sous forme de lignes table: (donnees) => { if (!Array.isArray(donnees)) return ecrire("", donnees); donnees.forEach((element, i) => ecrire("", `[${i}]`, element)); }, }; } /* ===================================================================== 4. EXÉCUTER LE CODE d'un éditeur ===================================================================== Comment exécuter du texte comme si c'était du code ? On crée une fonction à partir d'une chaîne de caractères. AsyncFunction est le "constructeur" des fonctions async : il permet d'utiliser "await" directement dans les exemples (chapitre 12). On passe à cette fonction nos propres "console" et "scene", qui REMPLACENT les vrais à l'intérieur du code exécuté. ⚠ Exécuter du texte comme du code est dangereux avec du texte venant d'inconnus. Ici, c'est TOI qui écris le code, donc pas de souci. --------------------------------------------------------------------- */ const AsyncFunction = (async function () {}).constructor; async function executer(bac) { // querySelector cherche le PREMIER élément correspondant au sélecteur CSS, // mais seulement À L'INTÉRIEUR de "bac". const editeur = bac.querySelector("textarea"); const sortie = bac.querySelector(".sortie"); const scene = bac.querySelector(".scene"); // peut être null s'il n'y en a pas sortie.innerHTML = ""; // on efface l'ancienne sortie // Si l'éditeur a une scène, on la remet dans son état de départ. // (dataset.initial = l'attribut data-initial, rempli au chargement) if (scene) scene.innerHTML = scene.dataset.initial; const maConsole = creerConsole(sortie); // try / catch : on "essaie" un bloc de code ; si une erreur survient, // au lieu de tout planter, on "attrape" l'erreur dans catch. try { const fonction = new AsyncFunction("console", "scene", editeur.value); await fonction(maConsole, scene); } catch (erreur) { maConsole.error(`${erreur.name} : ${erreur.message}`); const aide = expliquerErreur(erreur); if (aide) maConsole.info("💡 " + aide); } } /* ===================================================================== 5. BRANCHER chaque éditeur : boutons, raccourcis clavier ===================================================================== document.querySelectorAll renvoie TOUS les éléments correspondants. On boucle dessus avec forEach. --------------------------------------------------------------------- */ function preparerEditeurs() { document.querySelectorAll(".bac").forEach((bac) => { const editeur = bac.querySelector("textarea"); const scene = bac.querySelector(".scene"); // On mémorise le code d'origine pour le bouton « Réinitialiser ». // .trim() enlève les espaces / retours à la ligne au début et à la fin. editeur.value = editeur.value.trim(); const codeOriginal = editeur.value; // On mémorise aussi le contenu de départ de la scène if (scene) scene.dataset.initial = scene.innerHTML; // L'éditeur s'adapte à la hauteur de son code ajusterHauteur(editeur); editeur.addEventListener("input", () => ajusterHauteur(editeur)); // Désactive le correcteur orthographique (inutile pour du code) editeur.spellcheck = false; // --- Bouton « Exécuter » --- bac.querySelector(".executer").addEventListener("click", () => executer(bac)); // --- Bouton « Réinitialiser » --- bac.querySelector(".reinit").addEventListener("click", () => { editeur.value = codeOriginal; ajusterHauteur(editeur); bac.querySelector(".sortie").innerHTML = ""; if (scene) scene.innerHTML = scene.dataset.initial; }); // --- Raccourcis clavier dans l'éditeur --- // L'événement "keydown" se déclenche quand on enfonce une touche. // L'objet "evenement" décrit la touche (evenement.key) et les // touches spéciales tenues (ctrlKey, metaKey = Cmd sur Mac…). editeur.addEventListener("keydown", (evenement) => { // Ctrl+Entrée (ou Cmd+Entrée) → exécuter if (evenement.key === "Enter" && (evenement.ctrlKey || evenement.metaKey)) { evenement.preventDefault(); // empêche le retour à la ligne executer(bac); } // Tab → insérer 2 espaces au lieu de quitter le champ // (Échap puis Tab permet quand même de sortir au clavier) if (evenement.key === "Tab" && !evenement.shiftKey && !editeur.dataset.sortir) { evenement.preventDefault(); const debut = editeur.selectionStart; // position du curseur const fin = editeur.selectionEnd; editeur.value = editeur.value.slice(0, debut) + " " + editeur.value.slice(fin); editeur.selectionStart = editeur.selectionEnd = debut + 2; } // Échap : le prochain Tab sortira de l'éditeur (accessibilité clavier) editeur.dataset.sortir = evenement.key === "Escape" ? "1" : ""; }); }); } /* Fait grandir le