Export Web/SCORM : corrige le chemin de l'icône doublé "static/static/"
Build and deploy / test-python (push) Successful in 6m44s
Build and deploy / test-js (push) Successful in 56s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Le premier correctif ("static/icons/x.svg") ne suffisait pas : cette URL
est posée en style inline (--icon-url) mais CONSOMMÉE par `mask-image:
var(--icon-url)` dans static/style.css (.icon-svg) — une URL relative
dans une propriété personnalisée CSS se résout par rapport à la feuille
de style où le var() est UTILISÉ, pas où elle est définie (piège CSS
connu). Résultat : "static/icons/x.svg" redevenait
"static/static/icons/x.svg" une fois résolu depuis static/style.css
(CORS bloqué — deuxième rapport du même utilisateur). Seul "icons/x.svg"
(sans le préfixe "static/") est correct une fois résolu depuis
static/style.css. Corrigé côté rendu initial
(_relativize_absolute_urls) et côté port JS (forgeRenderIcone, qui
régénère ce widget après une action).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-02 10:12:44 +02:00
co-authored by Claude Sonnet 5
parent e08c53e042
commit b60fe63a54
4 changed files with 37 additions and 13 deletions
+13 -1
View File
@@ -67,9 +67,21 @@ def _relativize_absolute_urls(html, slug):
ailleurs que dans une URL réellement générée par le moteur. Sans ce ailleurs que dans une URL réellement générée par le moteur. Sans ce
correctif, un index.html ouvert en local (file://) échouait à charger correctif, un index.html ouvert en local (file://) échouait à charger
icônes/sprites/fichiers envoyés (CORS bloque une requête absolue vers icônes/sprites/fichiers envoyés (CORS bloque une requête absolue vers
file:///static/... — "la requête CORS n'utilise pas HTTP").""" file:///static/... — "la requête CORS n'utilise pas HTTP").
Cas particulier des icônes (render_icone.py) : leur URL est posée dans
un style INLINE (--icon-url), mais CONSUMÉE par `mask-image:
var(--icon-url)` dans static/style.css (voir .icon-svg) — une URL
relative dans une PROPRIÉTÉ PERSONNALISÉE CSS se résout par rapport à
la feuille de style où le var() est UTILISÉ, pas où elle est définie
(piège CSS connu). "icons/x.svg" (juste "/static/" retiré, sans le
garder) reste donc correct une fois résolu depuis static/style.css,
alors que "static/icons/x.svg" y redevient "static/static/icons/x.svg"
(bug signalé — CORS bloqué sur ce chemin doublé). Traité EN PREMIER,
plus spécifique que le remplacement générique juste après."""
return ( return (
html.replace(f"/game/{slug}/uploads/", "uploads/") html.replace(f"/game/{slug}/uploads/", "uploads/")
.replace("/static/icons/", "icons/")
.replace("/static/", "static/") .replace("/static/", "static/")
) )
@@ -55,7 +55,7 @@ test('icône — repli sur "star" si _icon_slug absent', () => {
const html = forgeRenderElementHtml(el, baseGameData(), {}, {}); const html = forgeRenderElementHtml(el, baseGameData(), {}, {});
// le style entier passe par forgeHtmlEscape (comme html.escape() côté // le style entier passe par forgeHtmlEscape (comme html.escape() côté
// serveur) : l'apostrophe de url('...') devient &#x27;. // serveur) : l'apostrophe de url('...') devient &#x27;.
assert.ok(html.includes("--icon-url:url(&#x27;static/icons/star.svg&#x27;)")); assert.ok(html.includes("--icon-url:url(&#x27;icons/star.svg&#x27;)"));
}); });
test('onglets — un panneau actif, les autres masqués', () => { test('onglets — un panneau actif, les autres masqués', () => {
@@ -73,13 +73,17 @@ function forgeRenderIcone(el, meta, gameData, childrenMap, ctx, parentFlexDirect
var existingClass = attrs.class || ""; var existingClass = attrs.class || "";
delete attrs.class; delete attrs.class;
attrs.class = (existingClass + " icon-svg").trim(); attrs.class = (existingClass + " icon-svg").trim();
// Chemin RELATIF (jamais "/static/...") : ce fichier ne s'exécute que // Chemin RELATIF, et surtout PAS préfixé "static/" : cette URL est
// dans le paquet exporté (window.FORGE_OFFLINE, voir render-element.js), // posée dans --icon-url (style inline) mais CONSOMMÉE par `mask-image:
// ouvert en local (file://) sans racine de serveur — un chemin absolu y // var(--icon-url)` dans static/style.css (voir .icon-svg) — une URL
// déclenche un blocage CORS ("la requête CORS n'utilise pas HTTP"), // relative dans une propriété personnalisée CSS se résout par rapport à
// exactement le bug qui affectait aussi le rendu initial côté serveur // LA FEUILLE DE STYLE où le var() est utilisé (static/style.css), pas
// (voir _relativize_absolute_urls dans publish/build_scorm_package.py). // où elle est définie (piège CSS connu) : "static/icons/x.svg" y
var iconUrl = "static/icons/" + iconSlug + ".svg"; // redeviendrait "static/static/icons/x.svg" (404/CORS bloqué — bug
// signalé). "icons/x.svg" est correct une fois résolu depuis
// static/style.css (voir aussi _relativize_absolute_urls dans
// publish/build_scorm_package.py, même correctif côté rendu initial).
var iconUrl = "icons/" + iconSlug + ".svg";
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " --icon-url:url('" + iconUrl + "');"; var style = forgeStyleString(el, parentFlexDirection, el.widget) + " --icon-url:url('" + iconUrl + "');";
return "<i " + forgeAttrString(attrs, style) + "></i>"; return "<i " + forgeAttrString(attrs, style) + "></i>";
} }
+12 -4
View File
@@ -56,9 +56,16 @@ def test_export_scorm_icon_widget_uses_a_relative_url(client, game):
part de la racine du serveur Flask) — jamais réécrit par le refactor part de la racine du serveur Flask) — jamais réécrit par le refactor
asset_url de templates/play.html (celui-ci ne touche que les <script>/ asset_url de templates/play.html (celui-ci ne touche que les <script>/
<link> du template, pas le rendered_html déjà bake dans le payload). <link> du template, pas le rendered_html déjà bake dans le payload).
Sans _relativize_absolute_urls (build_scorm_package.py), un index.html
ouvert en local (file://) déclenchait un blocage CORS sur cette icône Cette URL, posée en style INLINE (--icon-url), est CONSOMMÉE par
("la requête CORS n'utilise pas HTTP") — voir aussi le miroir JS `mask-image: var(--icon-url)` dans static/style.css (voir .icon-svg) —
une URL relative dans une propriété personnalisée CSS se résout par
rapport à LA FEUILLE DE STYLE où le var() est utilisé, pas où elle est
définie (piège CSS connu) : un premier correctif ("static/icons/x.svg")
redevenait donc "static/static/icons/x.svg" une fois résolu depuis
static/style.css (CORS bloqué sur ce chemin doublé — deuxième rapport
du même utilisateur) — seul "icons/x.svg" (sans "static/") est correct
une fois résolu depuis static/style.css. Voir aussi le miroir JS
(forgeRenderIcone, static/js/play/offline/render-special-widgets.js), (forgeRenderIcone, static/js/play/offline/render-special-widgets.js),
qui régénère ce même widget après une action, et devait être corrigé qui régénère ce même widget après une action, et devait être corrigé
séparément.""" séparément."""
@@ -72,7 +79,8 @@ def test_export_scorm_icon_widget_uses_a_relative_url(client, game):
import io import io
with zipfile.ZipFile(io.BytesIO(resp.data)) as zf: with zipfile.ZipFile(io.BytesIO(resp.data)) as zf:
html = zf.read("index.html").decode("utf-8") html = zf.read("index.html").decode("utf-8")
assert "static/icons/star.svg" in html assert "icons/star.svg" in html
assert "static/icons/star.svg" not in html
assert "/static/icons/star.svg" not in html assert "/static/icons/star.svg" not in html