663 lines
16 KiB
JavaScript
663 lines
16 KiB
JavaScript
/*
|
||
* CookieCode - apprendre l'algorithmique avec des recettes de cuisine.
|
||
* Microlibrairie sans dépendance, compatible navigateur, CDN et Node/CommonJS.
|
||
*/
|
||
((root, factory) => {
|
||
if (typeof module === "object" && module.exports) {
|
||
module.exports = factory();
|
||
} else {
|
||
root.CookieCode = factory();
|
||
}
|
||
})(typeof globalThis !== "undefined" ? globalThis : this, () => {
|
||
var configuration = {
|
||
sortie: "console",
|
||
afficherNotions: true,
|
||
};
|
||
|
||
function configurer(options) {
|
||
options = options || {};
|
||
if (Object.hasOwn(options, "sortie")) {
|
||
configuration.sortie = options.sortie;
|
||
}
|
||
if (Object.hasOwn(options, "afficherNotions")) {
|
||
configuration.afficherNotions = Boolean(options.afficherNotions);
|
||
}
|
||
return CookieCode;
|
||
}
|
||
|
||
function texte(valeur, defaut) {
|
||
if (valeur === undefined || valeur === null || valeur === "") {
|
||
return defaut || "";
|
||
}
|
||
return String(valeur);
|
||
}
|
||
|
||
function echapperHTML(valeur) {
|
||
return texte(valeur)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
function creerElement(type, nom, quantite, unite, valeur) {
|
||
if (!nom && nom !== 0) {
|
||
throw new Error(
|
||
`CookieCode : un nom est obligatoire pour creer un ${type}.`,
|
||
);
|
||
}
|
||
|
||
return {
|
||
type: type,
|
||
nom: texte(nom),
|
||
quantite: quantite,
|
||
unite: unite,
|
||
valeur: valeur,
|
||
};
|
||
}
|
||
|
||
function ingredient(nom, quantite, unite) {
|
||
return creerElement("ingredient", nom, quantite, unite);
|
||
}
|
||
|
||
function materiel(nom) {
|
||
return creerElement("materiel", nom);
|
||
}
|
||
|
||
function constante(nom, valeur) {
|
||
return creerElement("constante", nom, undefined, undefined, valeur);
|
||
}
|
||
|
||
function preparation(nom) {
|
||
return {
|
||
type: "preparation",
|
||
nom: texte(nom, "preparation"),
|
||
etat: {},
|
||
definir: function (cle, valeur) {
|
||
this.etat[cle] = valeur;
|
||
return this;
|
||
},
|
||
est: function (valeur) {
|
||
return (
|
||
this.etat.texture === valeur ||
|
||
this.etat.etat === valeur ||
|
||
this.etat[valeur] === true
|
||
);
|
||
},
|
||
};
|
||
}
|
||
|
||
function nomDe(element) {
|
||
if (element && typeof element === "object" && element.nom !== undefined) {
|
||
return texte(element.nom);
|
||
}
|
||
return texte(element);
|
||
}
|
||
|
||
function quantiteDe(element, quantite, unite) {
|
||
var q = quantite !== undefined ? quantite : element?.quantite;
|
||
var u = unite !== undefined ? unite : element?.unite;
|
||
|
||
if (q === undefined || q === null || q === "") {
|
||
return "";
|
||
}
|
||
|
||
return texte(q) + (u ? ` ${texte(u)}` : "");
|
||
}
|
||
|
||
function formatElement(element, quantite, unite) {
|
||
var quantiteTexte = quantiteDe(element, quantite, unite);
|
||
var nom = nomDe(element);
|
||
return quantiteTexte ? `${quantiteTexte} de ${nom}` : nom;
|
||
}
|
||
|
||
function conditionLisible(condition) {
|
||
if (typeof condition === "string") {
|
||
return condition;
|
||
}
|
||
if (typeof condition === "boolean") {
|
||
return condition ? "condition vraie" : "condition fausse";
|
||
}
|
||
if (typeof condition === "function") {
|
||
return "condition fournie";
|
||
}
|
||
return "condition";
|
||
}
|
||
|
||
function evaluerCondition(condition) {
|
||
if (typeof condition === "function") {
|
||
return Boolean(condition());
|
||
}
|
||
if (typeof condition === "boolean") {
|
||
return condition;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function creerEtape(texteEtape, notion, enfants) {
|
||
return {
|
||
texte: texteEtape,
|
||
notion: notion,
|
||
enfants: enfants || [],
|
||
};
|
||
}
|
||
|
||
function Recette(nom) {
|
||
if (!nom && nom !== 0) {
|
||
throw new Error("CookieCode : une recette doit avoir un nom.");
|
||
}
|
||
|
||
this.nom = texte(nom);
|
||
this.etapes = [];
|
||
this.notions = {
|
||
programme: true,
|
||
variables: [],
|
||
constantes: [],
|
||
fonctions: [],
|
||
boucles: false,
|
||
conditions: false,
|
||
sequence: true,
|
||
};
|
||
}
|
||
|
||
Recette.prototype._noterFonction = function (nom) {
|
||
if (this.notions.fonctions.indexOf(nom) === -1) {
|
||
this.notions.fonctions.push(nom);
|
||
}
|
||
};
|
||
|
||
Recette.prototype._noterVariable = function (nom) {
|
||
if (nom && this.notions.variables.indexOf(nom) === -1) {
|
||
this.notions.variables.push(nom);
|
||
}
|
||
};
|
||
|
||
Recette.prototype._noterConstante = function (nom) {
|
||
if (nom && this.notions.constantes.indexOf(nom) === -1) {
|
||
this.notions.constantes.push(nom);
|
||
}
|
||
};
|
||
|
||
Recette.prototype._ajouterEtape = function (texteEtape, notion, enfants) {
|
||
this.etapes.push(creerEtape(texteEtape, notion, enfants));
|
||
return this;
|
||
};
|
||
|
||
Recette.prototype.prendre = function (nom, quantite, unite) {
|
||
this._noterVariable(nomDe(nom));
|
||
this._noterFonction("prendre");
|
||
return this._ajouterEtape(
|
||
`Prendre ${formatElement(nom, quantite, unite)}.`,
|
||
"variable",
|
||
);
|
||
};
|
||
|
||
Recette.prototype.utiliser = function (outil) {
|
||
this._noterConstante(nomDe(outil));
|
||
this._noterFonction("utiliser");
|
||
return this._ajouterEtape(`Utiliser ${nomDe(outil)}.`, "constante");
|
||
};
|
||
|
||
Recette.prototype.ajouter = function (element, quantite, unite) {
|
||
this._noterFonction("ajouter");
|
||
if (element && element.type === "ingredient") {
|
||
this._noterVariable(element.nom);
|
||
}
|
||
return this._ajouterEtape(
|
||
`Ajouter ${formatElement(element, quantite, unite)}.`,
|
||
"fonction",
|
||
);
|
||
};
|
||
|
||
Recette.prototype.retirer = function (element) {
|
||
this._noterFonction("retirer");
|
||
return this._ajouterEtape(`Retirer ${nomDe(element)}.`, "fonction");
|
||
};
|
||
|
||
Recette.prototype.couper = function (element, forme) {
|
||
this._noterFonction("couper");
|
||
return this._ajouterEtape(
|
||
`Couper ${nomDe(element)}${forme ? ` en ${texte(forme)}` : ""}.`,
|
||
"fonction",
|
||
);
|
||
};
|
||
|
||
Recette.prototype.melanger = function () {
|
||
this._noterFonction("melanger");
|
||
return this._ajouterEtape("Melanger.", "fonction");
|
||
};
|
||
|
||
Recette.prototype.verser = function (contenu, contenant) {
|
||
this._noterFonction("verser");
|
||
return this._ajouterEtape(
|
||
"Verser " +
|
||
nomDe(contenu) +
|
||
(contenant ? ` dans ${nomDe(contenant)}` : "") +
|
||
".",
|
||
"fonction",
|
||
);
|
||
};
|
||
|
||
Recette.prototype.chauffer = function (element, temperature) {
|
||
this._noterFonction("chauffer");
|
||
return this._ajouterEtape(
|
||
"Chauffer " +
|
||
nomDe(element) +
|
||
(temperature ? ` a ${nomDe(temperature)}` : "") +
|
||
".",
|
||
"fonction",
|
||
);
|
||
};
|
||
|
||
Recette.prototype.cuire = function (duree, unite) {
|
||
this._noterFonction("cuire");
|
||
return this._ajouterEtape(
|
||
"Cuire" +
|
||
(duree
|
||
? ` pendant ${texte(duree)}${unite ? ` ${texte(unite)}` : ""}`
|
||
: "") +
|
||
".",
|
||
"fonction",
|
||
);
|
||
};
|
||
|
||
Recette.prototype.attendre = function (duree, unite) {
|
||
this._noterFonction("attendre");
|
||
return this._ajouterEtape(
|
||
`Attendre ${texte(duree)}${unite ? ` ${texte(unite)}` : ""}.`,
|
||
"fonction",
|
||
);
|
||
};
|
||
|
||
Recette.prototype.repeter = function (nombre, action) {
|
||
this.notions.boucles = true;
|
||
this._noterFonction("repeter");
|
||
|
||
var bloc = new Recette("bloc repeter");
|
||
if (typeof action === "function") {
|
||
action(bloc);
|
||
}
|
||
|
||
this._fusionnerNotions(bloc);
|
||
return this._ajouterEtape(
|
||
`Repeter ${texte(nombre)} fois :`,
|
||
"boucle",
|
||
bloc.etapes,
|
||
);
|
||
};
|
||
|
||
Recette.prototype.si = function (condition, action, sinonAction) {
|
||
this.notions.conditions = true;
|
||
this._noterFonction("si");
|
||
|
||
var bloc = new Recette("bloc si");
|
||
var blocSinon = new Recette("bloc sinon");
|
||
var conditionVraie = evaluerCondition(condition);
|
||
|
||
if (conditionVraie && typeof action === "function") {
|
||
action(bloc);
|
||
}
|
||
|
||
if (!conditionVraie && typeof sinonAction === "function") {
|
||
sinonAction(blocSinon);
|
||
}
|
||
|
||
this._fusionnerNotions(bloc);
|
||
this._fusionnerNotions(blocSinon);
|
||
|
||
var enfants = bloc.etapes;
|
||
if (blocSinon.etapes.length > 0) {
|
||
enfants = enfants.concat([
|
||
creerEtape("Sinon :", "condition", blocSinon.etapes),
|
||
]);
|
||
}
|
||
|
||
return this._ajouterEtape(
|
||
`Si ${conditionLisible(condition)} :`,
|
||
"condition",
|
||
enfants,
|
||
);
|
||
};
|
||
|
||
Recette.prototype.servir = function (element) {
|
||
this._noterFonction("servir");
|
||
this._ajouterEtape(
|
||
`Servir${element ? ` ${nomDe(element)}` : ""}.`,
|
||
"fonction",
|
||
);
|
||
return this.afficher();
|
||
};
|
||
|
||
Recette.prototype.afficher = function () {
|
||
var cible;
|
||
|
||
if (configuration.sortie === "console" || !configuration.sortie) {
|
||
if (typeof console !== "undefined" && console.log) {
|
||
console.log(this.toText());
|
||
}
|
||
return this;
|
||
}
|
||
|
||
if (
|
||
typeof configuration.sortie === "string" &&
|
||
typeof document !== "undefined"
|
||
) {
|
||
cible = document.querySelector(configuration.sortie);
|
||
if (cible) {
|
||
cible.innerHTML = this.toHTML();
|
||
}
|
||
}
|
||
|
||
return this;
|
||
};
|
||
|
||
Recette.prototype._fusionnerNotions = function (autreRecette) {
|
||
autreRecette.notions.variables.forEach((nom) => {
|
||
this._noterVariable(nom);
|
||
});
|
||
autreRecette.notions.constantes.forEach((nom) => {
|
||
this._noterConstante(nom);
|
||
});
|
||
autreRecette.notions.fonctions.forEach((nom) => {
|
||
this._noterFonction(nom);
|
||
});
|
||
this.notions.boucles = this.notions.boucles || autreRecette.notions.boucles;
|
||
this.notions.conditions =
|
||
this.notions.conditions || autreRecette.notions.conditions;
|
||
};
|
||
|
||
Recette.prototype._lignesEtapes = function (etapes, prefixe, indentation) {
|
||
var lignes = [];
|
||
etapes.forEach(function (etape, index) {
|
||
var numero = prefixe ? `${prefixe}.${index + 1}` : String(index + 1);
|
||
lignes.push(`${indentation + numero}. ${etape.texte}`);
|
||
if (etape.enfants && etape.enfants.length > 0) {
|
||
lignes = lignes.concat(
|
||
this._lignesEtapes(etape.enfants, numero, `${indentation} `),
|
||
);
|
||
}
|
||
}, this);
|
||
return lignes;
|
||
};
|
||
|
||
Recette.prototype.analyser = function () {
|
||
var actions = this.notions.fonctions.slice();
|
||
var etapesAPlates = aplatirEtapes(this.etapes);
|
||
var derniereAction =
|
||
etapesAPlates.length > 0
|
||
? actionDeTexte(etapesAPlates[etapesAPlates.length - 1].texte)
|
||
: "";
|
||
var erreurs = [];
|
||
|
||
if (this.etapes.length === 0) {
|
||
erreurs.push("Ta recette est vide : ajoute au moins une etape.");
|
||
}
|
||
|
||
if (this.etapes.length > 0 && derniereAction !== "servir") {
|
||
erreurs.push("Ta recette ne se termine pas par servir.");
|
||
}
|
||
|
||
etapesAPlates.forEach((etape, index) => {
|
||
if (
|
||
actionDeTexte(etape.texte) === "servir" &&
|
||
index !== etapesAPlates.length - 1
|
||
) {
|
||
erreurs.push("Tu as servi avant la fin de la recette.");
|
||
}
|
||
});
|
||
|
||
return {
|
||
nom: this.nom,
|
||
ingredients: this.notions.variables.slice(),
|
||
variables: this.notions.variables.slice(),
|
||
materiels: this.notions.constantes.slice(),
|
||
constantes: this.notions.constantes.slice(),
|
||
actions: actions,
|
||
nombreEtapes: etapesAPlates.length,
|
||
utiliseBoucle: this.notions.boucles,
|
||
utiliseCondition: this.notions.conditions,
|
||
termineParServir: derniereAction === "servir",
|
||
erreurs: erreurs,
|
||
};
|
||
};
|
||
|
||
Recette.prototype._lignesNotions = function () {
|
||
var lignes = [
|
||
"",
|
||
"Notions utilisees :",
|
||
"- Recette = programme",
|
||
"- Sequence = les etapes sont executees dans l'ordre",
|
||
];
|
||
|
||
if (this.notions.variables.length > 0) {
|
||
lignes.push(
|
||
`- Ingredients = variables : ${this.notions.variables.join(", ")}`,
|
||
);
|
||
}
|
||
if (this.notions.constantes.length > 0) {
|
||
lignes.push(
|
||
"- Materiel / valeurs fixes = constantes : " +
|
||
this.notions.constantes.join(", "),
|
||
);
|
||
}
|
||
if (this.notions.fonctions.length > 0) {
|
||
lignes.push(
|
||
`- Actions = fonctions : ${this.notions.fonctions.join(", ")}`,
|
||
);
|
||
}
|
||
if (this.notions.boucles) {
|
||
lignes.push("- Repeter = boucle");
|
||
}
|
||
if (this.notions.conditions) {
|
||
lignes.push("- Si = condition");
|
||
}
|
||
|
||
return lignes;
|
||
};
|
||
|
||
Recette.prototype.toText = function () {
|
||
var lignes = [`Recette : ${this.nom}`, "", "Etapes :"];
|
||
lignes = lignes.concat(this._lignesEtapes(this.etapes, "", ""));
|
||
|
||
if (configuration.afficherNotions) {
|
||
lignes = lignes.concat(this._lignesNotions());
|
||
}
|
||
|
||
return lignes.join("\n");
|
||
};
|
||
|
||
Recette.prototype.toHTML = function () {
|
||
var html = '<section class="cookiecode-recette">';
|
||
html += `<h2>Recette : ${echapperHTML(this.nom)}</h2>`;
|
||
html += "<h3>Etapes</h3>";
|
||
html += etapesHTML(this.etapes);
|
||
|
||
if (configuration.afficherNotions) {
|
||
html += "<h3>Notions utilisees</h3><ul>";
|
||
this._lignesNotions()
|
||
.slice(2)
|
||
.forEach((ligne) => {
|
||
html += `<li>${echapperHTML(ligne.replace(/^- /, ""))}</li>`;
|
||
});
|
||
html += "</ul>";
|
||
}
|
||
|
||
html += "</section>";
|
||
return html;
|
||
};
|
||
|
||
function aplatirEtapes(etapes) {
|
||
var resultat = [];
|
||
etapes.forEach((etape) => {
|
||
resultat.push(etape);
|
||
if (etape.enfants && etape.enfants.length > 0) {
|
||
resultat = resultat.concat(aplatirEtapes(etape.enfants));
|
||
}
|
||
});
|
||
return resultat;
|
||
}
|
||
|
||
function actionDeTexte(texteEtape) {
|
||
var index;
|
||
var valeur = texte(texteEtape).toLowerCase();
|
||
var actions = [
|
||
"prendre",
|
||
"utiliser",
|
||
"ajouter",
|
||
"retirer",
|
||
"couper",
|
||
"melanger",
|
||
"verser",
|
||
"chauffer",
|
||
"cuire",
|
||
"attendre",
|
||
"repeter",
|
||
"si",
|
||
"servir",
|
||
];
|
||
|
||
for (index = 0; index < actions.length; index += 1) {
|
||
if (valeur.indexOf(actions[index]) === 0) {
|
||
return actions[index];
|
||
}
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
function defi(nom, regles) {
|
||
if (!nom && nom !== 0) {
|
||
throw new Error("CookieCode : un defi doit avoir un nom.");
|
||
}
|
||
|
||
return {
|
||
type: "defi",
|
||
nom: texte(nom),
|
||
regles: regles || {},
|
||
};
|
||
}
|
||
|
||
function verifier(recetteAVerifier, defiAVerifier) {
|
||
var rapport = recetteAVerifier.analyser();
|
||
var regles = defiAVerifier?.regles ? defiAVerifier.regles : {};
|
||
var messages = [];
|
||
var reussi = true;
|
||
|
||
function valider(condition, succes, echec) {
|
||
if (condition) {
|
||
messages.push(`✅ ${succes}`);
|
||
} else {
|
||
reussi = false;
|
||
messages.push(`❌ ${echec}`);
|
||
}
|
||
}
|
||
|
||
if (regles.ingredientsMinimum !== undefined) {
|
||
valider(
|
||
rapport.ingredients.length >= regles.ingredientsMinimum,
|
||
`Tu as utilise ${rapport.ingredients.length} ingredient(s).`,
|
||
`Il faut au moins ${regles.ingredientsMinimum} ingredient(s).`,
|
||
);
|
||
}
|
||
|
||
if (regles.materielsMinimum !== undefined) {
|
||
valider(
|
||
rapport.materiels.length >= regles.materielsMinimum,
|
||
`Tu as utilise ${rapport.materiels.length} materiel(s).`,
|
||
`Il faut au moins ${regles.materielsMinimum} materiel(s).`,
|
||
);
|
||
}
|
||
|
||
(regles.actionsObligatoires || []).forEach((action) => {
|
||
valider(
|
||
rapport.actions.indexOf(action) !== -1,
|
||
`Action trouvee : ${action}.`,
|
||
`Action manquante : ${action}.`,
|
||
);
|
||
});
|
||
|
||
if (regles.boucleObligatoire) {
|
||
valider(
|
||
rapport.utiliseBoucle,
|
||
"Tu as utilise une boucle avec repeter.",
|
||
"Il manque une boucle : essaie avec .repeter().",
|
||
);
|
||
}
|
||
|
||
if (regles.conditionObligatoire) {
|
||
valider(
|
||
rapport.utiliseCondition,
|
||
"Tu as utilise une condition avec si.",
|
||
"Il manque une condition : essaie avec .si().",
|
||
);
|
||
}
|
||
|
||
if (regles.terminerParServir) {
|
||
valider(
|
||
rapport.termineParServir,
|
||
"Ta recette se termine par servir.",
|
||
"Ta recette doit se terminer par .servir().",
|
||
);
|
||
}
|
||
|
||
if (regles.interdireErreurs) {
|
||
valider(
|
||
rapport.erreurs.length === 0,
|
||
"Aucune erreur logique detectee.",
|
||
rapport.erreurs.join(" ") || "Une erreur logique a ete detectee.",
|
||
);
|
||
}
|
||
|
||
if (messages.length === 0) {
|
||
messages.push("ℹ️ Aucun critere de defi a verifier.");
|
||
}
|
||
|
||
return {
|
||
reussi: reussi,
|
||
nom: defiAVerifier ? defiAVerifier.nom : "Defi",
|
||
messages: messages,
|
||
rapport: rapport,
|
||
toText: function () {
|
||
return [
|
||
`Defi : ${this.nom}`,
|
||
this.reussi ? "Resultat : reussi" : "Resultat : a ameliorer",
|
||
]
|
||
.concat(this.messages)
|
||
.join("\n");
|
||
},
|
||
};
|
||
}
|
||
|
||
function etapesHTML(etapes) {
|
||
var html = "<ol>";
|
||
etapes.forEach((etape) => {
|
||
html += `<li>${echapperHTML(etape.texte)}`;
|
||
if (etape.enfants && etape.enfants.length > 0) {
|
||
html += etapesHTML(etape.enfants);
|
||
}
|
||
html += "</li>";
|
||
});
|
||
html += "</ol>";
|
||
return html;
|
||
}
|
||
|
||
function recette(nom) {
|
||
return new Recette(nom);
|
||
}
|
||
|
||
var CookieCode = {
|
||
configurer: configurer,
|
||
recette: recette,
|
||
ingredient: ingredient,
|
||
materiel: materiel,
|
||
constante: constante,
|
||
preparation: preparation,
|
||
defi: defi,
|
||
verifier: verifier,
|
||
version: "0.2.0",
|
||
};
|
||
|
||
return CookieCode;
|
||
});
|