@mostajs/schema-form 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog — @mostajs/schema-form
2
2
 
3
+ ## 0.4.1 — 2026-10-09
4
+
5
+ **Corrigé** (audit du 09/10/2026) : `name`, `opts.id`, `opts.action`, `min`, `max`, `step`, `rows` sont échappés —
6
+ une valeur d'attribut ne peut plus fermer la balise. Essai T-STY-5.
7
+
3
8
  ## 0.4.0 — 08/10/2026
4
9
 
5
10
  **L'habillage devient malléable** (demande du fondateur) : un style imposé en ligne ne se surchargeait qu'à
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/schema-form",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Interface de SAISIE g\u00e9n\u00e9rique pilot\u00e9e par sch\u00e9ma (z\u00e9ro-d\u00e9p) : sections + champs typ\u00e9s (text/textarea/number/boolean/select/color/date/localized/repeater), chemins imbriqu\u00e9s, multilingue AR/FR/EN. Rendu HTML (renderForm) + relecture (readValues). R\u00e9utilisable par toute app @mostajs.",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",
package/src/index.js CHANGED
@@ -112,11 +112,11 @@ function renderControl(f, value, name, mode = 'inline', locale = 'fr') {
112
112
  const ph = f.placeholder ? ` placeholder="${escapeHtml(label(f.placeholder, locale))}"` : '';
113
113
  switch (f.type) {
114
114
  case 'textarea':
115
- return `<textarea name="${name}"${req}${ph} rows="${f.rows || 4}"${at(mode, 'textarea')}>${escapeHtml(value ?? '')}</textarea>`;
115
+ return `<textarea name="${escapeHtml(name)}"${req}${ph} rows="${escapeHtml(f.rows || 4)}"${at(mode, 'textarea')}>${escapeHtml(value ?? '')}</textarea>`;
116
116
  case 'number':
117
- return `<input type="number" name="${name}"${req}${ph} value="${escapeHtml(value ?? '')}"${f.min != null ? ` min="${f.min}"` : ''}${f.max != null ? ` max="${f.max}"` : ''}${f.step != null ? ` step="${f.step}"` : ''}${at(mode, 'input')}>`;
117
+ return `<input type="number" name="${escapeHtml(name)}"${req}${ph} value="${escapeHtml(value ?? '')}"${f.min != null ? ` min="${escapeHtml(f.min)}"` : ''}${f.max != null ? ` max="${escapeHtml(f.max)}"` : ''}${f.step != null ? ` step="${escapeHtml(f.step)}"` : ''}${at(mode, 'input')}>`;
118
118
  case 'boolean':
119
- return `<label${at(mode, 'check')}><input type="checkbox" name="${name}" value="true"${value ? ' checked' : ''}> ${escapeHtml(label(f.help, locale) || label(MOTS.oui, locale))}</label>`;
119
+ return `<label${at(mode, 'check')}><input type="checkbox" name="${escapeHtml(name)}" value="true"${value ? ' checked' : ''}> ${escapeHtml(label(f.help, locale) || label(MOTS.oui, locale))}</label>`;
120
120
  // ── LE CHAMP FICHIER (0.3.0) ──────────────────────────────────────────
121
121
  //
122
122
  // ⚠️ `capture` ouvre l'APPAREIL PHOTO sur mobile, au lieu de la galerie. C'est ce qu'on veut
@@ -127,12 +127,12 @@ function renderControl(f, value, name, mode = 'inline', locale = 'fr') {
127
127
  // décide quel fichier est sélectionné (ce serait une exfiltration). Un formulaire de
128
128
  // modification doit donc afficher la pièce DÉJÀ envoyée à côté du champ, pas dedans.
129
129
  case 'fichier':
130
- return `<input type="file" name="${name}"${req}`
130
+ return `<input type="file" name="${escapeHtml(name)}"${req}`
131
131
  + `${f.accept ? ` accept="${escapeHtml(f.accept)}"` : ''}`
132
132
  + `${f.capture ? ` capture="${escapeHtml(f.capture)}"` : ''}`
133
133
  + `${f.multiple ? ' multiple' : ''}${at(mode, 'input')}>`;
134
134
  case 'select':
135
- return `<select name="${name}"${req}${at(mode, 'input')}>${(f.options || []).map((o) => `<option value="${escapeHtml(o.value)}"${String(value) === String(o.value) ? ' selected' : ''}>${escapeHtml(label(o.label ?? o.value, locale))}</option>`).join('')}</select>`;
135
+ return `<select name="${escapeHtml(name)}"${req}${at(mode, 'input')}>${(f.options || []).map((o) => `<option value="${escapeHtml(o.value)}"${String(value) === String(o.value) ? ' selected' : ''}>${escapeHtml(label(o.label ?? o.value, locale))}</option>`).join('')}</select>`;
136
136
  case 'montant': {
137
137
  // Un texte, PAS un `number` : le contrôle numérique du navigateur rend un flottant, et un
138
138
  // flottant n'est pas un montant. La conversion en unités mineures se fait à la relecture,
@@ -140,17 +140,17 @@ function renderControl(f, value, name, mode = 'inline', locale = 'fr') {
140
140
  const dec = f.decimales ?? 2;
141
141
  const aff = value === undefined || value === null || value === '' ? '' : affichageMontant(value, dec);
142
142
  return `<div${at(mode, 'montant')}>
143
- <input type="text" inputmode="decimal" name="${name}"${req} value="${escapeHtml(aff)}" placeholder="${escapeHtml(label(f.placeholder, locale) || (dec ? '0,00' : '0'))}"${at(mode, 'input')}>
143
+ <input type="text" inputmode="decimal" name="${escapeHtml(name)}"${req} value="${escapeHtml(aff)}" placeholder="${escapeHtml(label(f.placeholder, locale) || (dec ? '0,00' : '0'))}"${at(mode, 'input')}>
144
144
  ${f.devise ? `<span${at(mode, 'devise')}>${escapeHtml(f.devise)}</span>` : ''}</div>`;
145
145
  }
146
146
  case 'color':
147
- return `<input type="color" name="${name}" value="${escapeHtml(value || '#000000')}"${at(mode, 'couleur')}>`;
147
+ return `<input type="color" name="${escapeHtml(name)}" value="${escapeHtml(value || '#000000')}"${at(mode, 'couleur')}>`;
148
148
  case 'date':
149
- return `<input type="date" name="${name}"${req} value="${escapeHtml(value ?? '')}"${at(mode, 'input')}>`;
149
+ return `<input type="date" name="${escapeHtml(name)}"${req} value="${escapeHtml(value ?? '')}"${at(mode, 'input')}>`;
150
150
  case 'localized': {
151
151
  const locs = f.locales || LOCALES;
152
152
  return `<div${at(mode, 'localized')}>${locs.map((lc) =>
153
- `<div${at(mode, 'loc')}><span${at(mode, 'loc-tag', 'locTag')}>${lc}</span><input name="${name}.${lc}" dir="${lc === 'ar' ? 'rtl' : 'ltr'}" value="${escapeHtml((value && value[lc]) ?? '')}"${at(mode, 'input')}></div>`).join('')}</div>`;
153
+ `<div${at(mode, 'loc')}><span${at(mode, 'loc-tag', 'locTag')}>${lc}</span><input name="${escapeHtml(`${name}.${lc}`)}" dir="${lc === 'ar' ? 'rtl' : 'ltr'}" value="${escapeHtml((value && value[lc]) ?? '')}"${at(mode, 'input')}></div>`).join('')}</div>`;
154
154
  }
155
155
  case 'repeater': {
156
156
  const arr = Array.isArray(value) ? value : [];
@@ -159,10 +159,10 @@ function renderControl(f, value, name, mode = 'inline', locale = 'fr') {
159
159
  // rien à cloner, et une liste qu'on ne peut pas allonger n'est pas une liste — c'est un
160
160
  // formulaire figé qui en a l'air.
161
161
  const gabarit = `<template class="sf-tpl">${renderRepeaterItem(f, {}, `${name}[__i__]`, 0, mode, locale)}</template>`;
162
- return `<div class="sf-repeater" data-name="${name}">${items}${gabarit}<button type="button"${at(mode, 'add')}>${escapeHtml(label(f.addLabel, locale) || label(MOTS.ajouter, locale))}</button></div>`;
162
+ return `<div class="sf-repeater" data-name="${escapeHtml(name)}">${items}${gabarit}<button type="button"${at(mode, 'add')}>${escapeHtml(label(f.addLabel, locale) || label(MOTS.ajouter, locale))}</button></div>`;
163
163
  }
164
164
  default: // text
165
- return `<input type="text" name="${name}"${req}${ph} value="${escapeHtml(value ?? '')}"${at(mode, 'input')}>`;
165
+ return `<input type="text" name="${escapeHtml(name)}"${req}${ph} value="${escapeHtml(value ?? '')}"${at(mode, 'input')}>`;
166
166
  }
167
167
  }
168
168
  function renderRepeaterItem(f, item, prefix, i, mode = 'inline', locale = 'fr') {
@@ -248,7 +248,7 @@ export function renderForm(schema, values = {}, opts = {}) {
248
248
  // schéma — y compris DANS un répéteur — l'attribut est posé.
249
249
  const aUnFichier = JSON.stringify(schema).includes('"fichier"');
250
250
  const classe = `sf-form${opts.classe ? ` ${escapeHtml(opts.classe)}` : ''}`;
251
- return `<form class="${classe}"${opts.id ? ` id="${opts.id}"` : ''}${opts.action ? ` method="post" action="${opts.action}"` : ''}${aUnFichier ? ' enctype="multipart/form-data"' : ''}${locale === 'ar' ? ' dir="rtl"' : ''}>
251
+ return `<form class="${classe}"${opts.id ? ` id="${escapeHtml(opts.id)}"` : ''}${opts.action ? ` method="post" action="${escapeHtml(opts.action)}"` : ''}${aUnFichier ? ' enctype="multipart/form-data"' : ''}${locale === 'ar' ? ' dir="rtl"' : ''}>
252
252
  ${schema.title ? `<h2${at(mode, 'title')}>${escapeHtml(label(schema.title, locale))}</h2>` : ''}${body}${submit}</form>`;
253
253
  }
254
254