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
@@ -15,68 +15,83 @@
|
||||
// play.html) et sont routés depuis forgeRenderElementHtml() ci-dessous
|
||||
// via forgeSpecialRenderer().
|
||||
|
||||
var FORGE_LAYOUT_CAPABLE_WIDGETS = ["conteneur", "repeteur_donnees", "liste", "liste_numerotee", "groupe_champs", "tableau"];
|
||||
const FORGE_LAYOUT_CAPABLE_WIDGETS = ['conteneur', 'repeteur_donnees', 'liste', 'liste_numerotee', 'groupe_champs', 'tableau'];
|
||||
|
||||
// Widgets à balise simple portés à l'étape 3 — {tag, hasContent,
|
||||
// selfClosing, fixedAttrs} — port direct de screens/widgets/registry.py
|
||||
// (seuls les champs utiles au RENDU, pas au panneau de propriétés de
|
||||
// l'éditeur, qui ne concerne que le port serveur).
|
||||
var FORGE_SIMPLE_WIDGETS = {
|
||||
texte: { tag: "p", hasContent: true },
|
||||
titre: { tag: null, hasContent: true, fixedAttrs: { class: "title" } },
|
||||
bouton: { tag: "button", hasContent: true, fixedAttrs: { class: "button" } },
|
||||
lien: { tag: "a", hasContent: true },
|
||||
image: { tag: "img", hasContent: false, selfClosing: true },
|
||||
video: { tag: "video", hasContent: false },
|
||||
champ_texte: { tag: "input", hasContent: false, selfClosing: true, fixedAttrs: { type: "text", class: "input" } },
|
||||
champ_email: { tag: "input", hasContent: false, selfClosing: true, fixedAttrs: { type: "email", class: "input" } },
|
||||
champ_mot_de_passe: { tag: "input", hasContent: false, selfClosing: true, fixedAttrs: { type: "password", class: "input" } },
|
||||
zone_texte: { tag: "textarea", hasContent: false, fixedAttrs: { class: "textarea" } },
|
||||
liste: { tag: "ul", hasContent: true },
|
||||
liste_numerotee: { tag: "ol", hasContent: true },
|
||||
separateur: { tag: "hr", hasContent: false, selfClosing: true },
|
||||
conteneur: { tag: "div", hasContent: true, fixedAttrs: { class: "box" } },
|
||||
const FORGE_SIMPLE_WIDGETS = {
|
||||
texte: { tag: 'p', hasContent: true },
|
||||
titre: { tag: null, hasContent: true, fixedAttrs: { class: 'title' } },
|
||||
bouton: { tag: 'button', hasContent: true, fixedAttrs: { class: 'button' } },
|
||||
lien: { tag: 'a', hasContent: true },
|
||||
image: { tag: 'img', hasContent: false, selfClosing: true },
|
||||
video: { tag: 'video', hasContent: false },
|
||||
champ_texte: {
|
||||
tag: 'input', hasContent: false, selfClosing: true, fixedAttrs: { type: 'text', class: 'input' },
|
||||
},
|
||||
champ_email: {
|
||||
tag: 'input', hasContent: false, selfClosing: true, fixedAttrs: { type: 'email', class: 'input' },
|
||||
},
|
||||
champ_mot_de_passe: {
|
||||
tag: 'input', hasContent: false, selfClosing: true, fixedAttrs: { type: 'password', class: 'input' },
|
||||
},
|
||||
zone_texte: { tag: 'textarea', hasContent: false, fixedAttrs: { class: 'textarea' } },
|
||||
liste: { tag: 'ul', hasContent: true },
|
||||
liste_numerotee: { tag: 'ol', hasContent: true },
|
||||
separateur: { tag: 'hr', hasContent: false, selfClosing: true },
|
||||
conteneur: { tag: 'div', hasContent: true, fixedAttrs: { class: 'box' } },
|
||||
};
|
||||
|
||||
// widget -> clé de special_render (voir screens/widgets/registry.py).
|
||||
var FORGE_SPECIAL_RENDER_BY_WIDGET = {
|
||||
case_a_cocher: "checkbox_label", bouton_radio: "checkbox_label", liste_deroulante: "select_options",
|
||||
groupe_champs: "fieldset_legend", tableau: "table", repeteur_donnees: "repeater",
|
||||
jauge: "jauge", superposition: "overlay", onglets: "onglets", icone: "icone", personnage: "personnage",
|
||||
const FORGE_SPECIAL_RENDER_BY_WIDGET = {
|
||||
case_a_cocher: 'checkbox_label',
|
||||
bouton_radio: 'checkbox_label',
|
||||
liste_deroulante: 'select_options',
|
||||
groupe_champs: 'fieldset_legend',
|
||||
tableau: 'table',
|
||||
repeteur_donnees: 'repeater',
|
||||
jauge: 'jauge',
|
||||
superposition: 'overlay',
|
||||
onglets: 'onglets',
|
||||
icone: 'icone',
|
||||
personnage: 'personnage',
|
||||
};
|
||||
|
||||
// fixed_attrs des widgets à special_render (voir screens/widgets/registry.py)
|
||||
// — seul "tableau" en a un, tous les autres n'en ont aucun (contrairement
|
||||
// à "conteneur", qui ne doit JAMAIS servir de repli pour eux).
|
||||
var FORGE_SPECIAL_WIDGET_FIXED_ATTRS = { tableau: { class: "table" } };
|
||||
const FORGE_SPECIAL_WIDGET_FIXED_ATTRS = { tableau: { class: 'table' } };
|
||||
|
||||
function forgeHtmlEscape(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||
.replace(/"/g, """).replace(/'/g, "'");
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Port de apply_ctx.py — {{champ}} remplacé par la valeur de la ligne en
|
||||
// cours (contenu ET attributs), à l'intérieur d'un Répéteur.
|
||||
function forgeApplyCtx(text, ctx) {
|
||||
if (!ctx || !text) return text;
|
||||
var result = text;
|
||||
Object.keys(ctx).forEach(function (k) {
|
||||
if (k === "_forge_play_mode") return; // indicateur interne, pas un {{champ}}
|
||||
result = result.split("{{" + k + "}}").join(ctx[k]);
|
||||
let result = text;
|
||||
Object.keys(ctx).forEach((k) => {
|
||||
if (k === '_forge_play_mode') return; // indicateur interne, pas un {{champ}}
|
||||
result = result.split(`{{${k}}}`).join(ctx[k]);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function forgeAutoId(el) {
|
||||
return "elt-" + el.id;
|
||||
return `elt-${el.id}`;
|
||||
}
|
||||
|
||||
// Port de parent_flex_direction.py.
|
||||
function forgeParentFlexDirection(parentEl) {
|
||||
var style = (parentEl && parentEl.style) || {};
|
||||
if (style.display !== "flex") return null;
|
||||
return style["flex-direction"] || "row";
|
||||
const style = (parentEl && parentEl.style) || {};
|
||||
if (style.display !== 'flex') return null;
|
||||
return style['flex-direction'] || 'row';
|
||||
}
|
||||
|
||||
// Port de style_string.py — même logique de taille par défaut (100% hors
|
||||
@@ -84,34 +99,34 @@ function forgeParentFlexDirection(parentEl) {
|
||||
// enfant), même garde-fous (transform réservé au cadre extérieur pour un
|
||||
// élément posé directement sur l'écran, 0px ignoré = pas de surcharge).
|
||||
function forgeStyleString(el, parentFlexDirection, widgetKey) {
|
||||
var style = Object.assign({}, el.style || {});
|
||||
if (FORGE_LAYOUT_CAPABLE_WIDGETS.indexOf(widgetKey) !== -1 && !("display" in style)) {
|
||||
style.display = "flex";
|
||||
if (!("flex-direction" in style)) style["flex-direction"] = "column";
|
||||
const style = { ...el.style || {} };
|
||||
if (FORGE_LAYOUT_CAPABLE_WIDGETS.indexOf(widgetKey) !== -1 && !('display' in style)) {
|
||||
style.display = 'flex';
|
||||
if (!('flex-direction' in style)) style['flex-direction'] = 'column';
|
||||
}
|
||||
var isChild = !!el.parent_id;
|
||||
var baseStyle;
|
||||
const isChild = !!el.parent_id;
|
||||
let baseStyle;
|
||||
if (isChild) {
|
||||
var widthDefault = (parentFlexDirection === "row" || parentFlexDirection === "row-reverse") ? "auto" : "100%";
|
||||
baseStyle = "width:" + widthDefault + "; height:auto; box-sizing:border-box; margin:0;";
|
||||
const widthDefault = (parentFlexDirection === 'row' || parentFlexDirection === 'row-reverse') ? 'auto' : '100%';
|
||||
baseStyle = `width:${widthDefault}; height:auto; box-sizing:border-box; margin:0;`;
|
||||
} else {
|
||||
baseStyle = "width:100%; height:100%; box-sizing:border-box; margin:0;";
|
||||
baseStyle = 'width:100%; height:100%; box-sizing:border-box; margin:0;';
|
||||
}
|
||||
var hasSizeOverride = false;
|
||||
var parts = [];
|
||||
Object.keys(style).forEach(function (k) {
|
||||
var v = style[k];
|
||||
if (v === null || v === undefined || v === "") return;
|
||||
if (typeof v === "object") return; // filet de sécurité, voir style_string.py
|
||||
if (k === "transform" && !isChild) return;
|
||||
var strV = String(v).trim();
|
||||
if ((k === "width" || k === "height") && (strV === "0px" || strV === "0")) return;
|
||||
if (k === "width" || k === "height") hasSizeOverride = true;
|
||||
parts.push(k + ":" + v);
|
||||
let hasSizeOverride = false;
|
||||
const parts = [];
|
||||
Object.keys(style).forEach((k) => {
|
||||
const v = style[k];
|
||||
if (v === null || v === undefined || v === '') return;
|
||||
if (typeof v === 'object') return; // filet de sécurité, voir style_string.py
|
||||
if (k === 'transform' && !isChild) return;
|
||||
const strV = String(v).trim();
|
||||
if ((k === 'width' || k === 'height') && (strV === '0px' || strV === '0')) return;
|
||||
if (k === 'width' || k === 'height') hasSizeOverride = true;
|
||||
parts.push(`${k}:${v}`);
|
||||
});
|
||||
if (hasSizeOverride) parts.push("flex-shrink:0");
|
||||
var userStyle = parts.join("; ");
|
||||
return baseStyle + (userStyle ? " " + userStyle + ";" : "");
|
||||
if (hasSizeOverride) parts.push('flex-shrink:0');
|
||||
const userStyle = parts.join('; ');
|
||||
return baseStyle + (userStyle ? ` ${userStyle};` : '');
|
||||
}
|
||||
|
||||
// Port de attr_string.py. "alt" est le seul attribut dont la chaîne VIDE
|
||||
@@ -120,69 +135,67 @@ function forgeStyleString(el, parentFlexDirection, widgetKey) {
|
||||
// lecteur d'écran) — tous les autres attributs vides sont omis comme
|
||||
// avant (une valeur vide y équivaut à "non réglé").
|
||||
function forgeAttrString(attrs, extraStyle) {
|
||||
var parts = ['style="' + forgeHtmlEscape(extraStyle) + '"'];
|
||||
Object.keys(attrs).forEach(function (k) {
|
||||
var v = attrs[k];
|
||||
const parts = [`style="${forgeHtmlEscape(extraStyle)}"`];
|
||||
Object.keys(attrs).forEach((k) => {
|
||||
const v = attrs[k];
|
||||
if (v === null || v === undefined || v === false) return;
|
||||
if (v === "" && k !== "alt") return;
|
||||
parts.push(v === true ? forgeHtmlEscape(k) : forgeHtmlEscape(k) + '="' + forgeHtmlEscape(v) + '"');
|
||||
if (v === '' && k !== 'alt') return;
|
||||
parts.push(v === true ? forgeHtmlEscape(k) : `${forgeHtmlEscape(k)}="${forgeHtmlEscape(v)}"`);
|
||||
});
|
||||
return parts.join(" ");
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
// Port de visible_attrs.py.
|
||||
function forgeVisibleAttrs(el, meta, ctx) {
|
||||
var rawAttrs = el.attributes || {};
|
||||
var attrs = {};
|
||||
Object.keys(rawAttrs).forEach(function (k) {
|
||||
if (k.indexOf("_") === 0) return;
|
||||
var v = rawAttrs[k];
|
||||
attrs[k] = typeof v === "string" ? forgeApplyCtx(v, ctx) : v;
|
||||
const rawAttrs = el.attributes || {};
|
||||
const attrs = {};
|
||||
Object.keys(rawAttrs).forEach((k) => {
|
||||
if (k.indexOf('_') === 0) return;
|
||||
const v = rawAttrs[k];
|
||||
attrs[k] = typeof v === 'string' ? forgeApplyCtx(v, ctx) : v;
|
||||
});
|
||||
Object.assign(attrs, meta.fixedAttrs || {});
|
||||
var extraClasses = Object.keys(rawAttrs)
|
||||
.filter(function (k) { return k.indexOf("_class_") === 0 && rawAttrs[k]; })
|
||||
.map(function (k) { return rawAttrs[k]; });
|
||||
const extraClasses = Object.keys(rawAttrs)
|
||||
.filter((k) => k.indexOf('_class_') === 0 && rawAttrs[k])
|
||||
.map((k) => rawAttrs[k]);
|
||||
if (extraClasses.length) {
|
||||
var baseClass = attrs.class || "";
|
||||
attrs.class = (baseClass ? [baseClass] : []).concat(extraClasses).join(" ");
|
||||
const baseClass = attrs.class || '';
|
||||
attrs.class = (baseClass ? [baseClass] : []).concat(extraClasses).join(' ');
|
||||
}
|
||||
if (!("id" in attrs)) attrs.id = forgeAutoId(el);
|
||||
attrs["data-element-id"] = el.id;
|
||||
if (el.parent_id) attrs["data-anim-target"] = el.id;
|
||||
if (!('id' in attrs)) attrs.id = forgeAutoId(el);
|
||||
attrs['data-element-id'] = el.id;
|
||||
if (el.parent_id) attrs['data-anim-target'] = el.id;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
// Port de render_children.py.
|
||||
function forgeRenderChildren(el, gameData, childrenMap, ctx) {
|
||||
var kids = (childrenMap && childrenMap[el.id]) || [];
|
||||
var flexDir = forgeParentFlexDirection(el);
|
||||
return kids.map(function (c) {
|
||||
return forgeRenderElementHtml(c, gameData, childrenMap, ctx, flexDir);
|
||||
}).join("");
|
||||
const kids = (childrenMap && childrenMap[el.id]) || [];
|
||||
const flexDir = forgeParentFlexDirection(el);
|
||||
return kids.map((c) => forgeRenderElementHtml(c, gameData, childrenMap, ctx, flexDir)).join('');
|
||||
}
|
||||
|
||||
// Port de resolve_bound_row.py (_resolve_bound_row_ctx) — "Donnée liée"
|
||||
// d'un Texte/Titre : retrouve LA ligne qui correspond aux conditions
|
||||
// réglées (moteur partagé avec le Répéteur, voir filter-repeater-rows.js).
|
||||
function forgeResolveBoundRowCtx(gameData, attrs) {
|
||||
var definitionId = attrs._data_definition_id;
|
||||
let definitionId = attrs._data_definition_id;
|
||||
if (!definitionId) return null;
|
||||
definitionId = String(definitionId);
|
||||
var fieldsMetaForDef = gameData.fields_meta && gameData.fields_meta[definitionId];
|
||||
const fieldsMetaForDef = gameData.fields_meta && gameData.fields_meta[definitionId];
|
||||
if (!fieldsMetaForDef) return null;
|
||||
var rows = (gameData.data && gameData.data[definitionId]) || [];
|
||||
var decoded = forgeDecodeClauses(attrs, "_data_filtres_json", "_data_filtre_combinateur", "_data_filtre");
|
||||
let rows = (gameData.data && gameData.data[definitionId]) || [];
|
||||
const decoded = forgeDecodeClauses(attrs, '_data_filtres_json', '_data_filtre_combinateur', '_data_filtre');
|
||||
rows = forgeFilterRowsByClauses(gameData, definitionId, rows, decoded.clauses, decoded.combinator);
|
||||
if (!rows.length) return null;
|
||||
var row = rows[0];
|
||||
var ctx = {};
|
||||
fieldsMetaForDef.forEach(function (f) {
|
||||
const row = rows[0];
|
||||
const ctx = {};
|
||||
fieldsMetaForDef.forEach((f) => {
|
||||
// row[f.name] : gameData.data est déjà indexé par le nom D'AFFICHAGE
|
||||
// du champ (voir full_game_payload.py), jamais par sa colonne SQL —
|
||||
// voir filter-repeater-rows.js pour le détail de ce piège.
|
||||
var v = row[f.name];
|
||||
ctx[f.name] = (v === null || v === undefined || v === "") ? "" : String(v);
|
||||
const v = row[f.name];
|
||||
ctx[f.name] = (v === null || v === undefined || v === '') ? '' : String(v);
|
||||
});
|
||||
return ctx;
|
||||
}
|
||||
@@ -193,21 +206,19 @@ function forgeResolveBoundRowCtx(gameData, attrs) {
|
||||
// une fois à l'export, voir full_game_payload.py) plutôt que depuis
|
||||
// d'éventuels enfants copiés en base.
|
||||
function forgeRenderElementTypeChildren(el, gameData, ctx) {
|
||||
var templateTree = gameData.element_type_templates && gameData.element_type_templates[String(el.element_type_id)];
|
||||
const templateTree = gameData.element_type_templates && gameData.element_type_templates[String(el.element_type_id)];
|
||||
if (!templateTree || !templateTree.top.length) return [null, false];
|
||||
var isOverlayOnly = templateTree.top.every(function (c) { return c.widget === "superposition"; });
|
||||
var top = templateTree.top;
|
||||
const isOverlayOnly = templateTree.top.every((c) => c.widget === 'superposition');
|
||||
let { top } = templateTree;
|
||||
if (isOverlayOnly) {
|
||||
var overlayRoot = Object.assign({}, top[0]);
|
||||
var newStyle = {};
|
||||
Object.keys(overlayRoot.style || {}).forEach(function (k) { if (k !== "display") newStyle[k] = overlayRoot.style[k]; });
|
||||
const overlayRoot = { ...top[0] };
|
||||
const newStyle = {};
|
||||
Object.keys(overlayRoot.style || {}).forEach((k) => { if (k !== 'display') newStyle[k] = overlayRoot.style[k]; });
|
||||
overlayRoot.style = newStyle;
|
||||
top = top.slice();
|
||||
top[0] = overlayRoot;
|
||||
}
|
||||
var html = top.map(function (c) {
|
||||
return forgeRenderElementHtml(c, gameData, templateTree.children_map, ctx);
|
||||
}).join("");
|
||||
const html = top.map((c) => forgeRenderElementHtml(c, gameData, templateTree.children_map, ctx)).join('');
|
||||
return [html, isOverlayOnly];
|
||||
}
|
||||
|
||||
@@ -220,16 +231,16 @@ function forgeRenderElementTypeChildren(el, gameData, ctx) {
|
||||
// enfants/onglets/superposition).
|
||||
function forgeSpecialRenderer(specialKey) {
|
||||
switch (specialKey) {
|
||||
case "checkbox_label": return forgeRenderCheckboxOrRadio;
|
||||
case "select_options": return forgeRenderSelect;
|
||||
case "fieldset_legend": return forgeRenderFieldset;
|
||||
case "table": return forgeRenderTable;
|
||||
case "repeater": return forgeRenderRepeater;
|
||||
case "jauge": return forgeRenderJauge;
|
||||
case "overlay": return forgeRenderOverlay;
|
||||
case "onglets": return forgeRenderOnglets;
|
||||
case "icone": return forgeRenderIcone;
|
||||
case "personnage": return forgeRenderPersonnage;
|
||||
case 'checkbox_label': return forgeRenderCheckboxOrRadio;
|
||||
case 'select_options': return forgeRenderSelect;
|
||||
case 'fieldset_legend': return forgeRenderFieldset;
|
||||
case 'table': return forgeRenderTable;
|
||||
case 'repeater': return forgeRenderRepeater;
|
||||
case 'jauge': return forgeRenderJauge;
|
||||
case 'overlay': return forgeRenderOverlay;
|
||||
case 'onglets': return forgeRenderOnglets;
|
||||
case 'icone': return forgeRenderIcone;
|
||||
case 'personnage': return forgeRenderPersonnage;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -238,77 +249,78 @@ function forgeSpecialRenderer(specialKey) {
|
||||
// pas hors ligne (une seule partie en mémoire, pas de base de données) :
|
||||
// tout ce qui en dépendait côté serveur lit désormais `gameData`.
|
||||
function forgeRenderElementHtml(el, gameData, childrenMap, ctx, parentFlexDirection) {
|
||||
var widgetKey = el.widget;
|
||||
var simple = FORGE_SIMPLE_WIDGETS[widgetKey];
|
||||
var specialKey = FORGE_SPECIAL_RENDER_BY_WIDGET[widgetKey];
|
||||
var attrsRaw = el.attributes || {};
|
||||
const widgetKey = el.widget;
|
||||
const simple = FORGE_SIMPLE_WIDGETS[widgetKey];
|
||||
const specialKey = FORGE_SPECIAL_RENDER_BY_WIDGET[widgetKey];
|
||||
const attrsRaw = el.attributes || {};
|
||||
|
||||
var hasVisibilityCondition = attrsRaw._visibilite_cond_mode === "variable" || attrsRaw._visibilite_cond_mode === "objet";
|
||||
var playMode = !!((ctx || {})._forge_play_mode);
|
||||
const hasVisibilityCondition = attrsRaw._visibilite_cond_mode === 'variable' || attrsRaw._visibilite_cond_mode === 'objet';
|
||||
const playMode = !!((ctx || {})._forge_play_mode);
|
||||
if (playMode && hasVisibilityCondition && !forgeIsElementVisibleOffline(gameData, attrsRaw, ctx)) {
|
||||
return '<div class="visibilityGated" data-element-id="' + el.id + '" style="display:none;"></div>';
|
||||
return `<div class="visibilityGated" data-element-id="${el.id}" style="display:none;"></div>`;
|
||||
}
|
||||
var hasDataBinding = !!attrsRaw._data_definition_id;
|
||||
var mark = function (htmlStr) {
|
||||
var markers = "";
|
||||
if (hasVisibilityCondition) markers += "<!--visibilityGated-->";
|
||||
if (hasDataBinding) markers += "<!--dataBound-->";
|
||||
const hasDataBinding = !!attrsRaw._data_definition_id;
|
||||
const mark = function (htmlStr) {
|
||||
let markers = '';
|
||||
if (hasVisibilityCondition) markers += '<!--visibilityGated-->';
|
||||
if (hasDataBinding) markers += '<!--dataBound-->';
|
||||
return htmlStr + markers;
|
||||
};
|
||||
|
||||
var boundCtx = forgeResolveBoundRowCtx(gameData, attrsRaw);
|
||||
if (boundCtx) ctx = Object.assign({}, ctx || {}, boundCtx);
|
||||
const boundCtx = forgeResolveBoundRowCtx(gameData, attrsRaw);
|
||||
if (boundCtx) ctx = { ...ctx || {}, ...boundCtx };
|
||||
|
||||
// meta d'un widget à special_render : fixedAttrs propre (voir
|
||||
// screens/widgets/registry.py — seul "tableau" en a un, "class":"table"),
|
||||
// jamais celui de "conteneur" ("class":"box" polluerait tout le reste).
|
||||
var meta = simple || { fixedAttrs: FORGE_SPECIAL_WIDGET_FIXED_ATTRS[widgetKey] || {} };
|
||||
const meta = simple || { fixedAttrs: FORGE_SPECIAL_WIDGET_FIXED_ATTRS[widgetKey] || {} };
|
||||
if (specialKey) {
|
||||
var renderer = forgeSpecialRenderer(specialKey);
|
||||
const renderer = forgeSpecialRenderer(specialKey);
|
||||
return mark(renderer(el, meta, gameData, childrenMap, ctx, parentFlexDirection));
|
||||
}
|
||||
|
||||
var tag = meta.tag;
|
||||
let { tag } = meta;
|
||||
if (tag === null) { // widget à balise dynamique (ex: "titre" -> h1/h2/h3)
|
||||
var niveau = attrsRaw._niveau || "2";
|
||||
tag = "h" + niveau;
|
||||
const niveau = attrsRaw._niveau || '2';
|
||||
tag = `h${niveau}`;
|
||||
}
|
||||
|
||||
var attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
var style = forgeStyleString(el, parentFlexDirection, widgetKey);
|
||||
const attrs = forgeVisibleAttrs(el, meta, ctx);
|
||||
const style = forgeStyleString(el, parentFlexDirection, widgetKey);
|
||||
|
||||
var hoverText = attrsRaw._survol_texte;
|
||||
if (hoverText) attrs["data-hover-text"] = forgeApplyCtx(hoverText, ctx);
|
||||
const hoverText = attrsRaw._survol_texte;
|
||||
if (hoverText) attrs['data-hover-text'] = forgeApplyCtx(hoverText, ctx);
|
||||
|
||||
// alt (RGAA/WCAG 1.1.1) : le widget "image" n'a pas de champ de
|
||||
// description dédié dans l'éditeur — alt="" (décoratif) par défaut
|
||||
// plutôt qu'aucun attribut du tout, jamais le nom de fichier brut par
|
||||
// défaut du navigateur. Ne touche pas à un alt déjà posé (pass-through
|
||||
// via forgeVisibleAttrs, si l'auteur en règle un un jour).
|
||||
if (tag === "img" && !("alt" in attrs)) attrs.alt = "";
|
||||
if (tag === 'img' && !('alt' in attrs)) attrs.alt = '';
|
||||
|
||||
if (meta.selfClosing) {
|
||||
return mark("<" + tag + " " + forgeAttrString(attrs, style) + ">");
|
||||
return mark(`<${tag} ${forgeAttrString(attrs, style)}>`);
|
||||
}
|
||||
|
||||
var content = forgeApplyCtx(el.content || "", ctx);
|
||||
var childResult = el.element_type_id ? forgeRenderElementTypeChildren(el, gameData, ctx) : [null, false];
|
||||
var childHtml = childResult[0], isOverlayTemplate = childResult[1];
|
||||
const content = forgeApplyCtx(el.content || '', ctx);
|
||||
const childResult = el.element_type_id ? forgeRenderElementTypeChildren(el, gameData, ctx) : [null, false];
|
||||
let childHtml = childResult[0]; const
|
||||
isOverlayTemplate = childResult[1];
|
||||
if (childHtml === null) {
|
||||
childHtml = forgeRenderChildren(el, gameData, childrenMap, ctx);
|
||||
} else if (isOverlayTemplate) {
|
||||
delete attrs.class;
|
||||
return mark("<div " + forgeAttrString(attrs, style) + ">" + childHtml + "</div>");
|
||||
return mark(`<div ${forgeAttrString(attrs, style)}>${childHtml}</div>`);
|
||||
}
|
||||
|
||||
if (tag === "ul" || tag === "ol") {
|
||||
var items = content.split("\n").map(function (l) { return l.trim(); }).filter(Boolean);
|
||||
var inner = items.map(function (item) { return "<li>" + forgeHtmlEscape(item) + "</li>"; }).join("") + childHtml;
|
||||
return mark("<" + tag + " " + forgeAttrString(attrs, style) + ">" + inner + "</" + tag + ">");
|
||||
if (tag === 'ul' || tag === 'ol') {
|
||||
const items = content.split('\n').map((l) => l.trim()).filter(Boolean);
|
||||
const inner = items.map((item) => `<li>${forgeHtmlEscape(item)}</li>`).join('') + childHtml;
|
||||
return mark(`<${tag} ${forgeAttrString(attrs, style)}>${inner}</${tag}>`);
|
||||
}
|
||||
|
||||
var safeContent = content ? forgeHtmlEscape(content).replace(/\n/g, "<br>") : "";
|
||||
return mark("<" + tag + " " + forgeAttrString(attrs, style) + ">" + safeContent + childHtml + "</" + tag + ">");
|
||||
const safeContent = content ? forgeHtmlEscape(content).replace(/\n/g, '<br>') : '';
|
||||
return mark(`<${tag} ${forgeAttrString(attrs, style)}>${safeContent}${childHtml}</${tag}>`);
|
||||
}
|
||||
|
||||
// NOTE (node:test uniquement) : forgeRenderElementHtml() référence, pour
|
||||
@@ -324,14 +336,24 @@ function forgeRenderElementHtml(el, gameData, childrenMap, ctx, parentFlexDirect
|
||||
// fonctions ci-dessus pour leurs propres enfants/onglets/superposition).
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
forgeHtmlEscape, forgeApplyCtx, forgeAutoId, forgeParentFlexDirection, forgeStyleString,
|
||||
forgeAttrString, forgeVisibleAttrs, forgeRenderChildren, forgeRenderElementHtml,
|
||||
forgeResolveBoundRowCtx, forgeRenderElementTypeChildren, forgeSpecialRenderer,
|
||||
FORGE_SIMPLE_WIDGETS, FORGE_SPECIAL_RENDER_BY_WIDGET,
|
||||
forgeHtmlEscape,
|
||||
forgeApplyCtx,
|
||||
forgeAutoId,
|
||||
forgeParentFlexDirection,
|
||||
forgeStyleString,
|
||||
forgeAttrString,
|
||||
forgeVisibleAttrs,
|
||||
forgeRenderChildren,
|
||||
forgeRenderElementHtml,
|
||||
forgeResolveBoundRowCtx,
|
||||
forgeRenderElementTypeChildren,
|
||||
forgeSpecialRenderer,
|
||||
FORGE_SIMPLE_WIDGETS,
|
||||
FORGE_SPECIAL_RENDER_BY_WIDGET,
|
||||
};
|
||||
global.forgeIsElementVisibleOffline = global.forgeIsElementVisibleOffline
|
||||
|| require('./visibility-condition.js').forgeIsElementVisibleOffline;
|
||||
var filterMod = require('./filter-repeater-rows.js');
|
||||
const filterMod = require('./filter-repeater-rows.js');
|
||||
global.forgeResolveFilterValue = global.forgeResolveFilterValue || filterMod.forgeResolveFilterValue;
|
||||
global.forgeResolveVariablePath = global.forgeResolveVariablePath || filterMod.forgeResolveVariablePath;
|
||||
global.forgeDecodeClauses = global.forgeDecodeClauses || filterMod.forgeDecodeClauses;
|
||||
|
||||
Reference in New Issue
Block a user