Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
@@ -9,57 +9,56 @@
|
||||
// forgeParentFlexDirection/forgeRenderChildren), chargé avant ce fichier.
|
||||
|
||||
function forgeRenderCheckboxOrRadio(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
|
||||
var inputType = el.widget === "case_a_cocher" ? "checkbox" : "radio";
|
||||
var attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
const inputType = el.widget === 'case_a_cocher' ? 'checkbox' : 'radio';
|
||||
const attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
attrs.type = inputType;
|
||||
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " display:flex; align-items:center; gap:8px;";
|
||||
var content = forgeApplyCtx(el.content || "", ctx);
|
||||
return '<label ' + forgeAttrString({ class: inputType }, style) + '><input ' + forgeAttrString(attrs, "") + '> ' + forgeHtmlEscape(content) + '</label>';
|
||||
const style = `${forgeStyleString(el, parentFlexDirection, el.widget)} display:flex; align-items:center; gap:8px;`;
|
||||
const content = forgeApplyCtx(el.content || '', ctx);
|
||||
return `<label ${forgeAttrString({ class: inputType }, style)}><input ${forgeAttrString(attrs, '')}> ${forgeHtmlEscape(content)}</label>`;
|
||||
}
|
||||
|
||||
function forgeRenderSelect(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
|
||||
var outerAttrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
var bulmaClasses = outerAttrs.class || "";
|
||||
var selectAttrs = {}, wrapperAttrs = {};
|
||||
Object.keys(outerAttrs).forEach(function (k) {
|
||||
if (k === "id" || k === "data-element-id" || k === "data-anim-target") wrapperAttrs[k] = outerAttrs[k];
|
||||
else if (k !== "class") selectAttrs[k] = outerAttrs[k];
|
||||
const outerAttrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
const bulmaClasses = outerAttrs.class || '';
|
||||
const selectAttrs = {}; const
|
||||
wrapperAttrs = {};
|
||||
Object.keys(outerAttrs).forEach((k) => {
|
||||
if (k === 'id' || k === 'data-element-id' || k === 'data-anim-target') wrapperAttrs[k] = outerAttrs[k];
|
||||
else if (k !== 'class') selectAttrs[k] = outerAttrs[k];
|
||||
});
|
||||
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " display:flex;";
|
||||
var content = forgeApplyCtx(el.content || "", ctx);
|
||||
var options = content.split("\n").map(function (o) { return o.trim(); }).filter(Boolean);
|
||||
var optsHtml = options.map(function (o) {
|
||||
var esc = forgeHtmlEscape(o);
|
||||
return '<option value="' + esc + '">' + esc + '</option>';
|
||||
}).join("");
|
||||
var selectHtml = '<select ' + forgeAttrString(selectAttrs, "") + '>' + optsHtml + '</select>';
|
||||
var selectWrapClass = forgeHtmlEscape(("select is-fullwidth " + bulmaClasses).trim());
|
||||
return '<div ' + forgeAttrString(wrapperAttrs, style) + '><div class="' + selectWrapClass + '">' + selectHtml + '</div></div>';
|
||||
const style = `${forgeStyleString(el, parentFlexDirection, el.widget)} display:flex;`;
|
||||
const content = forgeApplyCtx(el.content || '', ctx);
|
||||
const options = content.split('\n').map((o) => o.trim()).filter(Boolean);
|
||||
const optsHtml = options.map((o) => {
|
||||
const esc = forgeHtmlEscape(o);
|
||||
return `<option value="${esc}">${esc}</option>`;
|
||||
}).join('');
|
||||
const selectHtml = `<select ${forgeAttrString(selectAttrs, '')}>${optsHtml}</select>`;
|
||||
const selectWrapClass = forgeHtmlEscape((`select is-fullwidth ${bulmaClasses}`).trim());
|
||||
return `<div ${forgeAttrString(wrapperAttrs, style)}><div class="${selectWrapClass}">${selectHtml}</div></div>`;
|
||||
}
|
||||
|
||||
function forgeRenderFieldset(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
|
||||
var attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
var style = forgeStyleString(el, parentFlexDirection, el.widget);
|
||||
var content = forgeApplyCtx(el.content || "", ctx) || "Groupe de champs";
|
||||
var childHtml = forgeRenderChildren(el, gameData, childrenMap, ctx);
|
||||
return '<fieldset ' + forgeAttrString(attrs, style) + '><legend>' + forgeHtmlEscape(content) + '</legend>' + childHtml + '</fieldset>';
|
||||
const attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
const style = forgeStyleString(el, parentFlexDirection, el.widget);
|
||||
const content = forgeApplyCtx(el.content || '', ctx) || 'Groupe de champs';
|
||||
const childHtml = forgeRenderChildren(el, gameData, childrenMap, ctx);
|
||||
return `<fieldset ${forgeAttrString(attrs, style)}><legend>${forgeHtmlEscape(content)}</legend>${childHtml}</fieldset>`;
|
||||
}
|
||||
|
||||
function forgeRenderTable(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
|
||||
var attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
var headerRow = ["1", "true", "on"].indexOf((el.attributes || {})._header_row) !== -1;
|
||||
var content = forgeApplyCtx(el.content || "", ctx);
|
||||
var style = forgeStyleString(el, parentFlexDirection, el.widget);
|
||||
var rows = content.split("\n").filter(function (r) { return r.trim(); });
|
||||
var htmlRows = rows.map(function (r, i) {
|
||||
var cells = r.split(";");
|
||||
var cellTag = (headerRow && i === 0) ? "th" : "td";
|
||||
var cellsHtml = cells.map(function (c) {
|
||||
return "<" + cellTag + ">" + forgeHtmlEscape(c.trim()) + "</" + cellTag + ">";
|
||||
}).join("");
|
||||
return "<tr>" + cellsHtml + "</tr>";
|
||||
}).join("");
|
||||
return '<table ' + forgeAttrString(attrs, style) + '>' + htmlRows + '</table>';
|
||||
const attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
const headerRow = ['1', 'true', 'on'].indexOf((el.attributes || {})._header_row) !== -1;
|
||||
const content = forgeApplyCtx(el.content || '', ctx);
|
||||
const style = forgeStyleString(el, parentFlexDirection, el.widget);
|
||||
const rows = content.split('\n').filter((r) => r.trim());
|
||||
const htmlRows = rows.map((r, i) => {
|
||||
const cells = r.split(';');
|
||||
const cellTag = (headerRow && i === 0) ? 'th' : 'td';
|
||||
const cellsHtml = cells.map((c) => `<${cellTag}>${forgeHtmlEscape(c.trim())}</${cellTag}>`).join('');
|
||||
return `<tr>${cellsHtml}</tr>`;
|
||||
}).join('');
|
||||
return `<table ${forgeAttrString(attrs, style)}>${htmlRows}</table>`;
|
||||
}
|
||||
|
||||
function forgeRenderIcone(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
|
||||
@@ -67,12 +66,12 @@ function forgeRenderIcone(el, meta, gameData, childrenMap, ctx, parentFlexDirect
|
||||
// catalogue curaté (screens/widgets/fontawesome_icons.py) : une valeur
|
||||
// invalide ne peut être enregistrée que depuis l'éditeur, qui la valide
|
||||
// déjà à la sauvegarde — un jeu EXPORTÉ n'en produit jamais.
|
||||
var rawAttrs = el.attributes || {};
|
||||
var iconSlug = rawAttrs._icon_slug || "star";
|
||||
var attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
var existingClass = attrs.class || "";
|
||||
const rawAttrs = el.attributes || {};
|
||||
const iconSlug = rawAttrs._icon_slug || 'star';
|
||||
const attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
const existingClass = attrs.class || '';
|
||||
delete attrs.class;
|
||||
attrs.class = (existingClass + " icon-svg").trim();
|
||||
attrs.class = (`${existingClass} icon-svg`).trim();
|
||||
// data: URI pré-calculée à l'export (voir gameData.icon_data_uris,
|
||||
// publish/build_scorm_package.py::_build_icon_data_uris) — jamais un
|
||||
// chemin de fichier, même relatif : Firefox traite chaque ressource
|
||||
@@ -84,49 +83,50 @@ function forgeRenderIcone(el, meta, gameData, childrenMap, ctx, parentFlexDirect
|
||||
// devrait pas arriver : _collect_icon_slugs scanne tous les widgets
|
||||
// "icone", posés directement ou nichés dans un élément de jeu
|
||||
// réutilisable) — au moins un lien exploitable plutôt qu'un vide.
|
||||
var iconUrl = (gameData.icon_data_uris && gameData.icon_data_uris[iconSlug]) || ("icons/" + iconSlug + ".svg");
|
||||
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " --icon-url:url('" + iconUrl + "');";
|
||||
const iconUrl = (gameData.icon_data_uris && gameData.icon_data_uris[iconSlug]) || (`icons/${iconSlug}.svg`);
|
||||
const style = `${forgeStyleString(el, parentFlexDirection, el.widget)} --icon-url:url('${iconUrl}');`;
|
||||
// aria-hidden (RGAA/WCAG 1.1.1) : une icône seule (pas de champ de
|
||||
// description dédié dans l'éditeur) est traitée comme purement
|
||||
// décorative — jamais annoncée par un lecteur d'écran, comme le nom du
|
||||
// slug FontAwesome ("star") n'aurait aucun sens lu à voix haute.
|
||||
if (!("aria-hidden" in attrs)) attrs["aria-hidden"] = "true";
|
||||
return "<i " + forgeAttrString(attrs, style) + "></i>";
|
||||
if (!('aria-hidden' in attrs)) attrs['aria-hidden'] = 'true';
|
||||
return `<i ${forgeAttrString(attrs, style)}></i>`;
|
||||
}
|
||||
|
||||
function forgeRenderOnglets(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
|
||||
var tabs = (childrenMap && childrenMap[el.id]) || [];
|
||||
var attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
var tabsExtraClasses = attrs.class || "";
|
||||
const tabs = (childrenMap && childrenMap[el.id]) || [];
|
||||
const attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
const tabsExtraClasses = attrs.class || '';
|
||||
delete attrs.class;
|
||||
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " display:flex; flex-direction:column;";
|
||||
var groupId = forgeAutoId(el);
|
||||
const style = `${forgeStyleString(el, parentFlexDirection, el.widget)} display:flex; flex-direction:column;`;
|
||||
const groupId = forgeAutoId(el);
|
||||
|
||||
if (!tabs.length) {
|
||||
return '<div ' + forgeAttrString(attrs, style) + '><em style="opacity:.6;">Onglets — ajoute au moins un onglet dans ses réglages.</em></div>';
|
||||
return `<div ${forgeAttrString(attrs, style)}><em style="opacity:.6;">Onglets — ajoute au moins un onglet dans ses réglages.</em></div>`;
|
||||
}
|
||||
|
||||
var flexDir = forgeParentFlexDirection(el);
|
||||
var barItems = [], panels = [];
|
||||
tabs.forEach(function (tab, i) {
|
||||
var label = tab.name || ("Onglet " + (i + 1));
|
||||
var active = i === 0;
|
||||
var tabHtml = forgeRenderElementHtml(tab, gameData, childrenMap, ctx, flexDir);
|
||||
var targetId = "tabpanel-" + tab.id;
|
||||
const flexDir = forgeParentFlexDirection(el);
|
||||
const barItems = []; const
|
||||
panels = [];
|
||||
tabs.forEach((tab, i) => {
|
||||
const label = tab.name || (`Onglet ${i + 1}`);
|
||||
const active = i === 0;
|
||||
const tabHtml = forgeRenderElementHtml(tab, gameData, childrenMap, ctx, flexDir);
|
||||
const targetId = `tabpanel-${tab.id}`;
|
||||
barItems.push(
|
||||
'<li class="' + (active ? "is-active" : "") + '" data-tab-btn="' + groupId + '" data-tab-target="' + targetId +
|
||||
'" onclick="forgeShowTab(this)"><a>' + forgeHtmlEscape(String(label)) + '</a></li>'
|
||||
`<li class="${active ? 'is-active' : ''}" data-tab-btn="${groupId}" data-tab-target="${targetId
|
||||
}" onclick="forgeShowTab(this)"><a>${forgeHtmlEscape(String(label))}</a></li>`,
|
||||
);
|
||||
var hiddenStyle = active ? "" : ' style="display:none;"';
|
||||
panels.push('<div class="forgeTabPanel" id="' + targetId + '" data-tab-group="' + groupId + '"' + hiddenStyle + '>' + tabHtml + '</div>');
|
||||
const hiddenStyle = active ? '' : ' style="display:none;"';
|
||||
panels.push(`<div class="forgeTabPanel" id="${targetId}" data-tab-group="${groupId}"${hiddenStyle}>${tabHtml}</div>`);
|
||||
});
|
||||
|
||||
var tabsClass = forgeHtmlEscape(("tabs " + tabsExtraClasses).trim());
|
||||
const tabsClass = forgeHtmlEscape((`tabs ${tabsExtraClasses}`).trim());
|
||||
return (
|
||||
'<div ' + forgeAttrString(attrs, style) + '>' +
|
||||
'<div class="' + tabsClass + '"><ul>' + barItems.join("") + '</ul></div>' +
|
||||
'<div class="forgeTabPanels" style="flex:1 1 auto; min-height:0;">' + panels.join("") + '</div>' +
|
||||
'</div>'
|
||||
`<div ${forgeAttrString(attrs, style)}>`
|
||||
+ `<div class="${tabsClass}"><ul>${barItems.join('')}</ul></div>`
|
||||
+ `<div class="forgeTabPanels" style="flex:1 1 auto; min-height:0;">${panels.join('')}</div>`
|
||||
+ '</div>'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,27 +136,32 @@ function forgeRenderOnglets(el, meta, gameData, childrenMap, ctx, parentFlexDire
|
||||
// travail est fait UNE fois, à la génération du paquet, jamais à chaque
|
||||
// rendu côté client.
|
||||
function forgeRenderPersonnage(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
|
||||
var attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
attrs["data-personnage"] = "1";
|
||||
var animations = gameData.personnage_animations && gameData.personnage_animations[String(el.id)];
|
||||
var frames = animations ? (animations.idle || animations[Object.keys(animations)[0]]) : null;
|
||||
var idleFrame = frames && frames.length ? frames[0] : "";
|
||||
const attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
attrs['data-personnage'] = '1';
|
||||
const animations = gameData.personnage_animations && gameData.personnage_animations[String(el.id)];
|
||||
const frames = animations ? (animations.idle || animations[Object.keys(animations)[0]]) : null;
|
||||
const idleFrame = frames && frames.length ? frames[0] : '';
|
||||
if (idleFrame) attrs.src = idleFrame;
|
||||
// alt (RGAA/WCAG 1.1.1) : voir render-element.js pour le même défaut
|
||||
// décoratif sur le widget "image" générique.
|
||||
if (!("alt" in attrs)) attrs.alt = "";
|
||||
var style = forgeStyleString(el, parentFlexDirection, el.widget);
|
||||
return "<img " + forgeAttrString(attrs, style) + ">";
|
||||
if (!('alt' in attrs)) attrs.alt = '';
|
||||
const style = forgeStyleString(el, parentFlexDirection, el.widget);
|
||||
return `<img ${forgeAttrString(attrs, style)}>`;
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
forgeRenderCheckboxOrRadio, forgeRenderSelect, forgeRenderFieldset, forgeRenderTable,
|
||||
forgeRenderIcone, forgeRenderOnglets, forgeRenderPersonnage,
|
||||
forgeRenderCheckboxOrRadio,
|
||||
forgeRenderSelect,
|
||||
forgeRenderFieldset,
|
||||
forgeRenderTable,
|
||||
forgeRenderIcone,
|
||||
forgeRenderOnglets,
|
||||
forgeRenderPersonnage,
|
||||
};
|
||||
var renderMod = require('./render-element.js');
|
||||
["forgeVisibleAttrs", "forgeStyleString", "forgeAttrString", "forgeApplyCtx", "forgeHtmlEscape",
|
||||
"forgeAutoId", "forgeParentFlexDirection", "forgeRenderChildren", "forgeRenderElementHtml"].forEach(function (name) {
|
||||
global[name] = global[name] || renderMod[name];
|
||||
});
|
||||
const renderMod = require('./render-element.js');
|
||||
['forgeVisibleAttrs', 'forgeStyleString', 'forgeAttrString', 'forgeApplyCtx', 'forgeHtmlEscape',
|
||||
'forgeAutoId', 'forgeParentFlexDirection', 'forgeRenderChildren', 'forgeRenderElementHtml'].forEach((name) => {
|
||||
global[name] = global[name] || renderMod[name];
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user