Initial CookieCode MVP
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/*.map
|
||||
.env
|
||||
.DS_Store
|
||||
@@ -0,0 +1,137 @@
|
||||
# CookieCode
|
||||
|
||||
CookieCode est une microlibrairie JavaScript educative pour apprendre les bases de l'algorithmique avec des recettes de cuisine.
|
||||
|
||||
L'idee :
|
||||
|
||||
| Cuisine | Programmation |
|
||||
| --- | --- |
|
||||
| Recette | Programme |
|
||||
| Ingredients | Variables |
|
||||
| Materiel | Constantes / ressources |
|
||||
| Etapes | Instructions |
|
||||
| Actions culinaires | Fonctions |
|
||||
| `si` | Condition |
|
||||
| `repeter` | Boucle |
|
||||
|
||||
## Objectif du MVP
|
||||
|
||||
- fonctionner directement dans une page HTML via une balise `<script>` ;
|
||||
- exposer une variable globale `CookieCode` ;
|
||||
- proposer une API chainable proche du langage naturel ;
|
||||
- afficher une trace de recette et une lecture algorithmique ;
|
||||
- rester sans dependance.
|
||||
|
||||
## Utilisation dans une page
|
||||
|
||||
```html
|
||||
<div id="app"></div>
|
||||
<script src="dist/cookiecode.js"></script>
|
||||
<script>
|
||||
CookieCode.configurer({
|
||||
sortie: "#app",
|
||||
afficherNotions: true
|
||||
});
|
||||
|
||||
CookieCode.recette("Cookie au chocolat")
|
||||
.prendre("farine", 200, "g")
|
||||
.prendre("sucre", 100, "g")
|
||||
.prendre("chocolat", 150, "g")
|
||||
.utiliser("saladier")
|
||||
.ajouter("farine")
|
||||
.ajouter("sucre")
|
||||
.ajouter("chocolat")
|
||||
.melanger()
|
||||
.repeter(2, function (recette) {
|
||||
recette.melanger();
|
||||
})
|
||||
.cuire(12, "minutes")
|
||||
.servir();
|
||||
</script>
|
||||
```
|
||||
|
||||
## API principale
|
||||
|
||||
### Creation
|
||||
|
||||
```js
|
||||
CookieCode.recette("Cookies");
|
||||
CookieCode.ingredient("farine", 200, "g");
|
||||
CookieCode.materiel("saladier");
|
||||
CookieCode.constante("temperature", 180);
|
||||
CookieCode.preparation("pate");
|
||||
```
|
||||
|
||||
### Methodes d'une recette
|
||||
|
||||
```js
|
||||
.prendre(nom, quantite, unite)
|
||||
.utiliser(materiel)
|
||||
.ajouter(element, quantite, unite)
|
||||
.retirer(element)
|
||||
.couper(element, forme)
|
||||
.melanger()
|
||||
.verser(contenu, contenant)
|
||||
.chauffer(element, temperature)
|
||||
.cuire(duree, unite)
|
||||
.attendre(duree, unite)
|
||||
.repeter(nombre, action)
|
||||
.si(condition, action, sinonAction)
|
||||
.servir(element)
|
||||
.afficher()
|
||||
.toText()
|
||||
.toHTML()
|
||||
```
|
||||
|
||||
## Exemple avec variables
|
||||
|
||||
```js
|
||||
const farine = CookieCode.ingredient("farine", 200, "g");
|
||||
const sucre = CookieCode.ingredient("sucre", 100, "g");
|
||||
const saladier = CookieCode.materiel("saladier");
|
||||
|
||||
const recette = CookieCode.recette("Cookies")
|
||||
.utiliser(saladier)
|
||||
.ajouter(farine)
|
||||
.ajouter(sucre)
|
||||
.melanger();
|
||||
|
||||
console.log(recette.toText());
|
||||
```
|
||||
|
||||
## Exemple avec boucle et condition
|
||||
|
||||
```js
|
||||
const pate = CookieCode.preparation("pate").definir("texture", "trop seche");
|
||||
|
||||
CookieCode.recette("Pate a cookies")
|
||||
.ajouter("farine", 200, "g")
|
||||
.si(function () {
|
||||
return pate.est("trop seche");
|
||||
}, function (recette) {
|
||||
recette.ajouter("lait", 20, "ml");
|
||||
})
|
||||
.repeter(3, function (recette) {
|
||||
recette.melanger();
|
||||
})
|
||||
.servir();
|
||||
```
|
||||
|
||||
## Cartes pedagogiques conseillees
|
||||
|
||||
| Type de carte | Mots cles | Notion |
|
||||
| --- | --- | --- |
|
||||
| Structure | `recette`, `servir` | programme / resultat |
|
||||
| Donnee | `ingredient`, `quantite`, `unite` | variable / valeur |
|
||||
| Materiel | `materiel`, `utiliser` | constante / ressource |
|
||||
| Action | `prendre`, `ajouter`, `couper`, `melanger`, `cuire` | fonction |
|
||||
| Logique | `si`, `sinon`, `repeter` | condition / boucle |
|
||||
|
||||
## Developpement
|
||||
|
||||
```sh
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
Aucune dependance n'est necessaire pour lancer les tests du MVP.
|
||||
Vendored
+477
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
* 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._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 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,
|
||||
version: "0.1.0",
|
||||
};
|
||||
|
||||
return CookieCode;
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
CookieCode.configurer({
|
||||
sortie: "#app",
|
||||
afficherNotions: true,
|
||||
});
|
||||
|
||||
CookieCode.recette("Cookie au chocolat")
|
||||
.prendre("farine", 200, "g")
|
||||
.prendre("sucre", 100, "g")
|
||||
.prendre("chocolat", 150, "g")
|
||||
.utiliser("saladier")
|
||||
.ajouter("farine")
|
||||
.ajouter("sucre")
|
||||
.ajouter("chocolat")
|
||||
.melanger()
|
||||
.repeter(2, (recette) => {
|
||||
recette.melanger();
|
||||
})
|
||||
.cuire(12, "minutes")
|
||||
.servir();
|
||||
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CookieCode - Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>CookieCode</h1>
|
||||
<p>Une recette devient un programme : les ingredients sont des variables, les actions sont des fonctions.</p>
|
||||
<div id="app" aria-live="polite"></div>
|
||||
</main>
|
||||
|
||||
<script src="../dist/cookiecode.js"></script>
|
||||
<script src="./demo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
Feature: Apprendre l'algorithmique avec CookieCode
|
||||
En tant qu'apprenant debutant
|
||||
Je veux ecrire une recette comme un programme
|
||||
Afin de comprendre les variables, fonctions, boucles et conditions
|
||||
|
||||
Scenario: Afficher une recette simple avec les notions de base
|
||||
Given une page integre CookieCode via une balise script
|
||||
When l'apprenant cree une recette avec des ingredients et des actions
|
||||
Then la recette affiche les etapes dans l'ordre
|
||||
And la lecture algorithmique indique que la recette est un programme
|
||||
And la lecture algorithmique indique que les ingredients sont des variables
|
||||
And la lecture algorithmique indique que les actions sont des fonctions
|
||||
|
||||
Scenario: Representer une boucle avec repeter
|
||||
Given une recette CookieCode
|
||||
When l'apprenant ajoute une etape repeter 2 fois
|
||||
Then la recette affiche une etape de repetition
|
||||
And les actions repetees sont affichees comme sous-etapes
|
||||
And la lecture algorithmique indique que repeter est une boucle
|
||||
|
||||
Scenario: Representer une condition avec si
|
||||
Given une preparation avec un etat observable
|
||||
When l'apprenant ajoute une condition vraie
|
||||
Then la recette affiche l'etape conditionnelle
|
||||
And les actions du bloc conditionnel sont affichees
|
||||
And la lecture algorithmique indique que si est une condition
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "cookiecode",
|
||||
"version": "0.1.0",
|
||||
"description": "Microlibrairie educative pour apprendre l'algorithmique avec des recettes de cuisine.",
|
||||
"main": "dist/cookiecode.js",
|
||||
"scripts": {
|
||||
"test": "node test/cookiecode.test.js",
|
||||
"build": "cp src/cookiecode.js dist/cookiecode.js"
|
||||
},
|
||||
"keywords": [
|
||||
"education",
|
||||
"algorithmique",
|
||||
"cuisine",
|
||||
"javascript",
|
||||
"cdn"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
-- Script simple en Lua pour afficher une recette de cuisine
|
||||
|
||||
local recette = {
|
||||
nom = "Omelette nature",
|
||||
personnes = 1,
|
||||
ingredients = {
|
||||
"2 oeufs",
|
||||
"1 pincee de sel",
|
||||
"1 pincee de poivre",
|
||||
"1 noisette de beurre"
|
||||
},
|
||||
etapes = {
|
||||
"Casser les oeufs dans un bol.",
|
||||
"Ajouter le sel et le poivre, puis battre avec une fourchette.",
|
||||
"Faire fondre le beurre dans une poele chaude.",
|
||||
"Verser les oeufs battus dans la poele.",
|
||||
"Cuire 2 a 3 minutes, puis servir chaud."
|
||||
}
|
||||
}
|
||||
|
||||
local function afficher_liste(titre, elements)
|
||||
print(titre)
|
||||
for index, element in ipairs(elements) do
|
||||
print(index .. ". " .. element)
|
||||
end
|
||||
print("")
|
||||
end
|
||||
|
||||
print("=== Recette de cuisine ===")
|
||||
print("Nom : " .. recette.nom)
|
||||
print("Pour : " .. recette.personnes .. " personne")
|
||||
print("")
|
||||
|
||||
afficher_liste("Ingredients :", recette.ingredients)
|
||||
afficher_liste("Etapes :", recette.etapes)
|
||||
|
||||
print("Bon appetit !")
|
||||
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
* 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._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 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,
|
||||
version: "0.1.0",
|
||||
};
|
||||
|
||||
return CookieCode;
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
const assert = require("assert");
|
||||
const CookieCode = require("../dist/cookiecode.js");
|
||||
|
||||
CookieCode.configurer({ sortie: null, afficherNotions: true });
|
||||
|
||||
const texteRecette = CookieCode.recette("Cookie au chocolat")
|
||||
.prendre("farine", 200, "g")
|
||||
.prendre("sucre", 100, "g")
|
||||
.utiliser("saladier")
|
||||
.ajouter("farine")
|
||||
.melanger()
|
||||
.repeter(2, (recette) => {
|
||||
recette.melanger();
|
||||
})
|
||||
.cuire(12, "minutes")
|
||||
.toText();
|
||||
|
||||
assert.match(texteRecette, /Recette : Cookie au chocolat/);
|
||||
assert.match(texteRecette, /1\. Prendre 200 g de farine\./);
|
||||
assert.match(texteRecette, /2\. Prendre 100 g de sucre\./);
|
||||
assert.match(texteRecette, /3\. Utiliser saladier\./);
|
||||
assert.match(texteRecette, /6\. Repeter 2 fois :/);
|
||||
assert.match(texteRecette, /6\.1\. Melanger\./);
|
||||
assert.match(texteRecette, /Ingredients = variables : farine, sucre/);
|
||||
assert.match(texteRecette, /Materiel \/ valeurs fixes = constantes : saladier/);
|
||||
assert.match(texteRecette, /Repeter = boucle/);
|
||||
|
||||
const pate = CookieCode.preparation("pate").definir("texture", "trop seche");
|
||||
const texteCondition = CookieCode.recette("Pate a cookies")
|
||||
.si(
|
||||
() => pate.est("trop seche"),
|
||||
(recette) => {
|
||||
recette.ajouter("lait", 20, "ml");
|
||||
},
|
||||
)
|
||||
.toText();
|
||||
|
||||
assert.match(texteCondition, /Si condition fournie :/);
|
||||
assert.match(texteCondition, /1\.1\. Ajouter 20 ml de lait\./);
|
||||
assert.match(texteCondition, /Si = condition/);
|
||||
|
||||
const html = CookieCode.recette("<script>alert('xss')</script>")
|
||||
.ajouter("<img src=x onerror=alert(1)>")
|
||||
.toHTML();
|
||||
|
||||
assert.ok(!html.includes("<script>alert"));
|
||||
assert.ok(!html.includes("<img src=x"));
|
||||
assert.ok(html.includes("<script>alert"));
|
||||
assert.ok(html.includes("<img src=x"));
|
||||
|
||||
assert.strictEqual(CookieCode.version, "0.1.0");
|
||||
|
||||
console.log("CookieCode tests: OK");
|
||||
Reference in New Issue
Block a user