@massimo-cassandro/minimo 1.45.1 → 1.46.0

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.
@@ -8,5 +8,6 @@
8
8
  ## svuota cache
9
9
  ```bash
10
10
  cd /path/to/project/dir
11
- sudo php bin/console cache:clear --env=prod
11
+ sudo php bin/console cache:clear # solo cache dell'ambiente configurato
12
+ sudo php bin/console cache:clear --env=prod # solo cache prod
12
13
  ```
@@ -14,6 +14,8 @@
14
14
  /config/reference.php
15
15
  /var/
16
16
  /vendor/
17
+ /.phpunit.cache/
18
+ .phpunit.result.cache
17
19
  ###< symfony/framework-bundle ###
18
20
 
19
21
 
@@ -24,3 +26,4 @@ _reports/**/*.html
24
26
  public/_dev
25
27
  unresolved-unused-props.md
26
28
  orphan-classes.md
29
+ .symfony.*.y*ml
@@ -0,0 +1,29 @@
1
+ # .symfony.local.yaml
2
+ #
3
+ # Worker Messenger avviato insieme a `symfony serve`.
4
+ # Si ferma con il server (Ctrl+C, oppure `symfony server:stop` se avviato con -d).
5
+ #
6
+ # SE IL WORKER RESTA ATTIVO DOPO LO SPEGNIMENTO DEL SERVER
7
+ # (terminale chiuso bruscamente, `kill -9`, crash del server):
8
+ #
9
+ # 1. Verificare se esiste un processo orfano:
10
+ # ps aux | grep messenger:consume | grep -v grep
11
+ #
12
+ # 2. Fermarlo (SIGTERM: Messenger termina il messaggio in corso prima di uscire):
13
+ # pkill -f messenger:consume
14
+ #
15
+ # 3. Se dopo qualche secondo è ancora presente, forzare l'arresto:
16
+ # pkill -9 -f messenger:consume
17
+ #
18
+ # Fare il controllo anche prima di riavviare il server, per evitare due
19
+ # consumer sulla stessa coda (quello vecchio non è più monitorato da `watch`
20
+ # e userebbe codice non aggiornato).
21
+ #
22
+ # Per avviare il server senza worker: symfony serve --no-workers
23
+
24
+ # workers:
25
+ # messenger:
26
+ # cmd: ['symfony', 'console', 'messenger:consume', 'async', '--time-limit=3600', '--memory-limit=256M', '-vv']
27
+ # watch: ['config', 'src', 'templates']
28
+ # webpack:
29
+ # cmd: ['npm', 'run', 'webpack DEV']
@@ -12,20 +12,12 @@ export default [
12
12
  // })),
13
13
 
14
14
  ...eslint_config,
15
- // {
16
- // files: ['src/**/*.js'],
17
- // ignores: [
18
- // 'dist/',
19
- // 'build/',
20
- // '**/vendor/'
21
- // ],
22
- // }
23
- // {
24
- // languageOptions: {
25
- // globals: {
26
- // jQuery: 'readonly',
27
- // $: 'readonly',
28
- // },
29
- // },
30
- // },
15
+ {
16
+ files: ['app/**/*.js'],
17
+ languageOptions: {
18
+ globals: {
19
+ process: 'readonly'
20
+ }
21
+ }
22
+ }
31
23
  ];
@@ -20,7 +20,7 @@ import CopyWebpackPlugin from 'copy-webpack-plugin';
20
20
  import RemoveEmptyScriptsPlugin from 'webpack-remove-empty-scripts';
21
21
  // import HtmlWebpackInjectAttributesPlugin from 'html-webpack-inject-attributes-plugin';
22
22
 
23
- // NB: symfony -> decommentare WebpackManifestPlugin
23
+ // SETUP symfony -> decommentare WebpackManifestPlugin
24
24
  // import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
25
25
 
26
26
  import { cssRules } from './webpack-config-modules/css-rules.mjs';
@@ -506,7 +506,7 @@ const config = {
506
506
  // SETUP symfony (decommentare)
507
507
  // =>> plugins: WebpackManifestPlugin
508
508
  // new WebpackManifestPlugin({
509
- // fileName: path.join(output_dir, 'manifest.json'), // NB: symfony -> fileName: 'manifest.json', // scrive in output.path
509
+ // fileName: path.join(output_dir, 'manifest.json'), // SETUP symfony -> fileName: 'manifest.json', // scrive in output.path
510
510
  // // basePath: item.source_dirname
511
511
  // // removeKeyHash: /\?.*$/, // /([a-f0-9]{32}\.?)/gi, // /(\?as_asset)$/,
512
512
  // // rimuove dal manifest i font non woff2 (non necessari, rendono il file inutilmente grande);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.45.1",
3
+ "version": "1.46.0",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -83,7 +83,7 @@
83
83
  "!**/TODO*"
84
84
  ],
85
85
  "peerDependencies": {
86
- "@svgdotjs/svg.js": "^3.2.7",
86
+ "@svgdotjs/svg.js": "^3.2.8",
87
87
  "blurhash": "^2.0.5",
88
88
  "simple-datatables": "^10.3.0",
89
89
  "style-dictionary": "^5.0.0"
@@ -104,27 +104,27 @@
104
104
  },
105
105
  "devDependencies": {
106
106
  "@massimo-cassandro/eslint-config": "^3.0.19",
107
- "@types/node": "^26.2.0",
108
- "jsdom": "^30.0.1",
109
- "style-dictionary": "^5.5.1"
107
+ "@types/node": "^26.6.5",
108
+ "jsdom": "^30.1.3",
109
+ "style-dictionary": "^5.7.0"
110
110
  },
111
111
  "dependencies": {
112
- "@inquirer/input": "^5.1.2",
113
- "@inquirer/select": "^5.2.1",
112
+ "@inquirer/input": "^5.1.7",
113
+ "@inquirer/select": "^5.2.6",
114
114
  "@stylistic/stylelint-config": "^5.0.0",
115
- "@stylistic/stylelint-plugin": "^5.3.0",
115
+ "@stylistic/stylelint-plugin": "^5.3.1",
116
116
  "clipboardy": "^5.3.2",
117
117
  "json5": "^2.2.3",
118
- "sharp": "^0.35.3",
118
+ "sharp": "^0.35.5",
119
119
  "sharp-ico": "^0.1.5",
120
- "stylelint": "^17.15.0",
120
+ "stylelint": "^17.16.0",
121
121
  "stylelint-config-css-modules": "^4.6.0",
122
122
  "stylelint-config-recess-order": "^7.8.0",
123
123
  "stylelint-config-standard": "^40.0.0",
124
124
  "stylelint-config-standard-scss": "^17.0.0",
125
- "stylelint-order": "^8.1.1",
126
- "stylelint-scss": "^7.2.0",
127
- "svgo": "^4.0.2"
125
+ "stylelint-order": "^8.1.2",
126
+ "stylelint-scss": "^7.3.0",
127
+ "svgo": "^4.1.0"
128
128
  },
129
129
  "directories": {
130
130
  "doc": "docs",
@@ -1,13 +1,14 @@
1
1
  /*! minimo - DOM Builder */
2
2
  import { domBuilderBasicSetup } from './domBuilderBasicSetup.js';
3
3
  import { parseDomString } from './parseDomString.js';
4
+ import { createDomElement, normalizeNamespace } from './resolveNamespace.js';
4
5
 
5
6
  // TODO sintassi stringa multi-riga in cui ogni riga corrisponde a un elemento
6
7
  // TODO come sopra, con nidificazione opzionale tramite indentazione
7
8
 
8
9
  /**
9
10
  * @typedef {Object} DomBuilderItem
10
- * @property {string | string[]} [tag='div'] - Nome del tag HTML o array di tag annidati (ciascuno è il genitore del successivo). Anche `tagName` (default: 'div')
11
+ * @property {string | string[]} [tag='div'] - Nome del tag o array di tag annidati (ciascuno è il genitore del successivo). Anche `tagName`. La grafia viene conservata, necessaria per i tag SVG come `linearGradient` (default: 'div')
11
12
  * @property {string | string[]} [tagName='div'] - Alias di `tag` (default: 'div')
12
13
  * @property {string | string[]} [className] - Classe/i CSS: una singola stringa o un array (i valori falsy vengono scartati).
13
14
  * @property {string | string[]} [class] - Alias di `className`.
@@ -20,12 +21,20 @@ import { parseDomString } from './parseDomString.js';
20
21
  * (`Element.setHTML`) dove supportata, con ripiego su `innerHTML` grezzo nei browser che non la hanno.
21
22
  * Un `Node` (un `Element`, un `DocumentFragment`, ...) viene aggiunto così com'è, direttamente o
22
23
  * restituito da una funzione.
24
+ * Il markup viene letto nel contesto dell'elemento: dentro un `svg` diventa SVG. Attenzione: la Sanitizer API
25
+ * rimuove `<use>` anche con `sanitizer: {}` (fa parte della baseline di sicurezza del browser), quindi per le
26
+ * icone SVG va usata la sintassi a oggetto (`{ tag: 'use', attrs: { href: '...' } }`), non una stringa di markup.
27
+ * @property {'html' | 'svg' | 'math' | string} [namespace] - Namespace dell'elemento: un alias (`'html'`, `'svg'`, `'math'`)
28
+ * o un URI completo. Se presente ha la precedenza sulla risoluzione automatica; i figli lo ereditano tramite il
29
+ * namespace del genitore. Con `tag` array si applica al primo elemento della catena (gli altri lo ereditano).
30
+ * Un valore non valido produce un `console.error` e l'elemento viene creato in HTML.
31
+ * Non disponibile nella sintassi abbreviata a stringa (default: assente, risoluzione automatica)
23
32
  * @property {string | number} [text] - Scorciatoia per un nodo di testo letterale: inserisce un nodo `Text` semplice (mai
24
33
  * interpretato come markup) al posto di un elemento, così da poter essere fratello dei tag negli array
25
34
  * `children`/`content`. Mutuamente esclusivo con `tag`/`content`/`children`: quando è impostato (e `tag` è assente),
26
35
  * ogni altra proprietà tranne `condition` e `callback` viene ignorata.
27
36
  * @property {boolean} [condition=true] - Se false, l'elemento (o il nodo di testo) viene saltato (default: true)
28
- * @property {((el: HTMLElement|Text) => void) | null} [callback] - Callback invocata dopo la creazione dell'elemento (o del nodo di testo).
37
+ * @property {((el: Element|Text) => void) | null} [callback] - Callback invocata dopo la creazione dell'elemento (o del nodo di testo).
29
38
  * @property {Array<DomBuilderItem|string|Node>} [children] - Array di configurazione degli elementi figli. Accetta stringhe (scorciatoia secondo `parseDomString`), oggetti di configurazione e/o `Node` (un `Element`, un `DocumentFragment`, ...) inseriti così come sono.
30
39
  */
31
40
 
@@ -85,6 +94,23 @@ import { parseDomString } from './parseDomString.js';
85
94
  * Quando la stringa abbreviata e le proprietà a livello di oggetto sono in conflitto (classi, id o attributi),
86
95
  * le proprietà a livello di oggetto hanno la precedenza.
87
96
  *
97
+ * **Namespace (SVG / MathML)**
98
+ *
99
+ * Gli elementi vengono creati nel namespace corretto, così che SVG e MathML vengano disegnati dal browser.
100
+ * Per ogni elemento, nell'ordine:
101
+ * 1. `item.namespace`, se presente;
102
+ * 2. `svg` usa sempre il namespace SVG e `math` sempre quello MathML, qualunque sia il genitore;
103
+ * 3. gli altri elementi prendono il namespace del genitore effettivo (nessuna lista di tag: `a`, `title`, `script`,
104
+ * `style` esistono sia in HTML sia in SVG e conta solo il contesto), tranne nei punti che riportano all'HTML:
105
+ * figli di `foreignObject`, figli di `mi`/`mo`/`mn`/`ms`/`mtext` (tranne `mglyph` e `malignmark`) e figli di
106
+ * `annotation-xml` con `encoding` `text/html` o `application/xhtml+xml`;
107
+ * 4. senza genitore, `options.namespace` se presente, altrimenti HTML.
108
+ *
109
+ * Il genitore effettivo è `parent` (con `insertMode` `before`/`after` è `parent.parentNode`), l'elemento precedente
110
+ * nella catena `tag: [...]` e l'elemento stesso per `children` e `content` array.
111
+ * Gli elementi HTML vengono creati con `document.createElement`, gli altri con `document.createElementNS`.
112
+ * La sintassi abbreviata a stringa non supporta `namespace`: usa solo la risoluzione automatica.
113
+ *
88
114
  * @function domBuilder
89
115
  * @param {Array<DomBuilderItem|string|Node>} [structureArray=[]] - Array di configurazione. Accetta stringhe (scorciatoia secondo `parseDomString`), oggetti di configurazione e/o `Node` (un `Element`, un `DocumentFragment`, ...) inseriti così come sono; ogni oggetto di configurazione supporta:
90
116
  * - `tag` / `tagName` {string | string[]} - Nome del tag HTML, o array di tag annidati (ciascuno è il genitore del successivo). Default `'div'`.
@@ -92,18 +118,59 @@ import { parseDomString } from './parseDomString.js';
92
118
  * - `id` {string | null} - ID univoco dell'elemento.
93
119
  * - `attrs` {[string, *] | [string, *][] | Object<string, *>} - Attributi: una coppia `[name, value]`, un array di coppie o un oggetto `{name: value}`.
94
120
  * - `content` {string | number | Function | Node | DomBuilderItem[] | null} - Contenuto dell'elemento. Una stringa/numero viene impostato come `textContent`, a meno che contenga `<`: in tal caso viene sanificato e inserito come markup (`Element.setHTML`, con ripiego su `innerHTML`). Un `Node` (`Element`, `DocumentFragment`, ...), passato direttamente o restituito da una funzione, viene aggiunto così com'è.
121
+ * - `namespace` {'html' | 'svg' | 'math' | string} - Namespace esplicito (alias o URI completo), con precedenza sulla risoluzione automatica; ereditato dai figli. Default: assente (risoluzione automatica).
95
122
  * - `text` {string | number} - Scorciatoia per un nodo di testo letterale: inserisce un nodo `Text` semplice (mai interpretato come markup) come fratello degli altri elementi, al posto di un elemento. Mutuamente esclusivo con `tag`/`content`/`children`.
96
123
  * - `condition` {boolean} - Se false, l'elemento (o il nodo di testo) viene saltato. Default `true`.
97
124
  * - `callback` {(function(HTMLElement|Text): void) | null} - Invocata dopo la creazione dell'elemento (o del nodo di testo).
98
125
  * - `children` {Array<DomBuilderItem|string|Node>} - Array di configurazione degli elementi figli (stesso formato, annidato; accetta anche `Node` inseriti così come sono).
99
126
  *
100
127
  * ID e classi possono essere indicati come chiavi di primo livello dell'oggetto o dentro `attrs`; le proprietà di primo livello hanno la precedenza (default: [])
101
- * @param {HTMLElement} [parent] - Elemento genitore a cui viene agganciata la struttura (vedi `options.insertMode`).
128
+ * @param {Element | null} [parent] - Elemento genitore a cui viene agganciata la struttura (vedi `options.insertMode`).
102
129
  * @param {Object} [options={}] - Opzioni di configurazione (default: {})
103
130
  * @param {boolean} [options.emptyParent=false] - Se true, l'elemento genitore viene svuotato prima della costruzione (default: false)
104
131
  * @param {'append'|'prepend'|'before'|'after'} [options.insertMode='append'] - Come ogni elemento radice di `structureArray` viene agganciato a `parent`: `append` lo inserisce come ultimo figlio (default), `prepend` come primo figlio, `before`/`after` lo inseriscono come fratello precedente/successivo di `parent`. In ogni modalità l'ordine di `structureArray` viene mantenuto. Si applica solo agli elementi prodotti da questa chiamata — le chiamate annidate a `domBuilder` (`content`, `children`) fanno sempre append (default: 'append')
105
132
  * @param {boolean} [options.debug=false] - Se true, scrive `structureArray` in console dopo che ogni elemento stringa è stato analizzato con `parseDomString` (cioè l'array elaborato effettivamente usato per costruire il DOM) (default: false)
106
- * @returns {HTMLElement|null} Il primo elemento creato, oppure null se non è stato creato nulla.
133
+ * @param {'html'|'svg'|'math'|string} [options.namespace] - Namespace iniziale (alias o URI completo) usato quando manca il genitore effettivo, ad esempio per costruire più fratelli SVG senza genitore. Non si applica quando il genitore c'è, né agli elementi `svg`/`math` o con `namespace` esplicito (default: assente, cioè HTML)
134
+ * @returns {HTMLElement|null} Il primo elemento creato, oppure null se non è stato creato nulla. Il tipo dichiarato è `HTMLElement` per comodità, ma per gli elementi SVG o MathML il valore restituito è un `SVGElement` o un `MathMLElement`.
135
+ *
136
+ * @example
137
+ * // riepilogo dei parametri con i valori di default
138
+ * domBuilder([
139
+ * {
140
+ * tag: 'div', // default: 'div'
141
+ * className: undefined,
142
+ * id: undefined,
143
+ * attrs: undefined,
144
+ * content: undefined,
145
+ * namespace: undefined, // default: risoluzione automatica
146
+ * condition: true, // default: true
147
+ * callback: undefined,
148
+ * children: undefined
149
+ * }
150
+ * ], parent, {
151
+ * emptyParent: false, // default: false
152
+ * insertMode: 'append', // default: 'append'
153
+ * debug: false, // default: false
154
+ * namespace: undefined // default: HTML in assenza di genitore
155
+ * });
156
+ *
157
+ * @example
158
+ * // icona SVG con `use`: `svg` è sempre nel namespace SVG, `title` e `use` lo ereditano
159
+ * domBuilder([{
160
+ * tag: 'svg',
161
+ * className: 'icon',
162
+ * children: [
163
+ * { tag: 'title', content: 'Carica' },
164
+ * { tag: 'use', attrs: { href: 'icons.svg#cloud' } }
165
+ * ]
166
+ * }], container);
167
+ *
168
+ * @example
169
+ * // frammento SVG senza genitore e senza `svg` come radice
170
+ * const use = domBuilder([{ tag: 'use', namespace: 'svg', attrs: { href: 'icons.svg#cloud' } }]);
171
+ *
172
+ * // più fratelli SVG senza genitore
173
+ * domBuilder(['path.a', 'circle.b'], null, { namespace: 'svg' });
107
174
  */
108
175
 
109
176
 
@@ -129,18 +196,27 @@ export function domBuilder(/** @type {Array<DomBuilderItem|string|Node>} */ stru
129
196
  */
130
197
  const useFragment = !!parent && (options.insertMode === 'append' || options.insertMode === 'prepend');
131
198
 
132
- /** @type {HTMLElement | DocumentFragment | null} */
199
+ /** @type {Element | DocumentFragment | null} */
133
200
  let target = useFragment ? document.createDocumentFragment() : (parent ?? null);
134
201
 
135
- /** @type {Map<HTMLElement | DocumentFragment, Node>} tiene traccia, per `insertMode: 'after'`, dell'ultimo fratello inserito per ogni nodo genitore */
202
+ /*
203
+ Genitore effettivo degli elementi radice, usato per risolverne il namespace: non `target`, che può essere
204
+ il DocumentFragment di raggruppamento (privo di namespace), ma `parent`, oppure il suo genitore con
205
+ `before`/`after`, dove gli elementi diventano fratelli di `parent`
206
+ */
207
+ const rootParent = !parent ? null
208
+ : (options.insertMode === 'before' || options.insertMode === 'after') ? parent.parentNode : parent;
209
+
210
+ // namespace iniziale in assenza di genitore effettivo
211
+ const fallbackNs = normalizeNamespace(options.namespace, 'options.namespace');
212
+
213
+ /** @type {Map<Element | DocumentFragment, Node>} tiene traccia, per `insertMode: 'after'`, dell'ultimo fratello inserito per ogni nodo genitore */
136
214
  const afterAnchors = new Map();
137
215
 
138
216
  /** @type {HTMLElement | null} */
139
217
  let mainElement = null;
140
218
  /** @type {HTMLElement} */
141
219
  let el;
142
- /** @type {HTMLElement | DocumentFragment | null} */
143
- let grand_parent = null;
144
220
 
145
221
  /** @type {Array<DomBuilderItem|null>} */
146
222
  const elaboratedStructureArray = [];
@@ -217,11 +293,22 @@ export function domBuilder(/** @type {Array<DomBuilderItem|string|Node>} */ stru
217
293
  return;
218
294
  }
219
295
 
296
+ // namespace esplicito dell'elemento (con `tag` array, del primo elemento della catena)
297
+ const itemNs = normalizeNamespace(safeItem.namespace, 'item.namespace');
298
+
299
+ /*
300
+ Con `tag` array: primo elemento della catena (inserito in `target` secondo `insertMode` al posto di `el`)
301
+ ed elemento che deve contenere `el`
302
+ */
303
+ /** @type {Element | null} */
304
+ let chainRoot = null;
305
+ /** @type {Element | null} */
306
+ let chainParent = null;
307
+
220
308
  // quando tag è un array, crea una serie di elementi annidati;
221
309
  // l'ultimo riceve le restanti proprietà dell'oggetto
222
310
  if (Array.isArray(safeItem.tag)) {
223
311
 
224
- grand_parent = target;
225
312
  const tags = /** @type {string[]} */ (safeItem.tag);
226
313
 
227
314
  tags.forEach((tagItem, idx) => {
@@ -234,22 +321,34 @@ export function domBuilder(/** @type {Array<DomBuilderItem|string|Node>} */ stru
234
321
  safeItem.attrs = {...parsedItem.attrs ?? {}, ...safeItem.attrs ?? {}};
235
322
  }
236
323
  el = domBuilderBasicSetup(
237
- document.createElement(parsedItem.tag || 'div'),
324
+ /** @type {HTMLElement} */ (createDomElement(
325
+ parsedItem.tag || 'div',
326
+ idx === 0 ? rootParent : chainParent,
327
+ idx === 0 ? itemNs : null,
328
+ fallbackNs
329
+ )),
238
330
  {...parsedItem, ...(isLast ? safeItem : {})}
239
331
  );
240
332
 
241
333
  if (!isLast) { // l'ultimo elemento viene gestito dal percorso standard qui sotto
242
- if (target) {
243
- target.appendChild(el);
334
+ if (chainParent) {
335
+ chainParent.appendChild(el);
336
+ } else {
337
+ chainRoot = el;
244
338
  }
245
- target = el;
339
+ chainParent = el;
246
340
  }
247
341
 
248
342
  });
249
343
 
250
344
  } else {
251
345
  el = domBuilderBasicSetup(
252
- document.createElement(/** @type {string} */ (safeItem.tag) ?? 'div'),
346
+ /** @type {HTMLElement} */ (createDomElement(
347
+ /** @type {string} */ (safeItem.tag) ?? 'div',
348
+ rootParent,
349
+ itemNs,
350
+ fallbackNs
351
+ )),
253
352
  safeItem
254
353
  );
255
354
  }
@@ -319,18 +418,24 @@ export function domBuilder(/** @type {Array<DomBuilderItem|string|Node>} */ stru
319
418
  mainElement = el;
320
419
  }
321
420
 
421
+ // con `tag` array, `el` va dentro la catena e in `target` viene inserito il primo elemento della catena
422
+ if (chainParent) {
423
+ chainParent.appendChild(el);
424
+ }
425
+ const rootNode = chainRoot ?? el;
426
+
322
427
  if (target) {
323
428
  if (options.insertMode === 'before') {
324
429
  const anchor = /** @type {HTMLElement} */ (target);
325
- anchor.parentNode?.insertBefore(el, anchor);
430
+ anchor.parentNode?.insertBefore(rootNode, anchor);
326
431
 
327
432
  } else if (options.insertMode === 'after') {
328
433
  const anchor = afterAnchors.get(/** @type {HTMLElement} */ (target)) ?? /** @type {HTMLElement} */ (target);
329
- anchor.parentNode?.insertBefore(el, anchor.nextSibling);
330
- afterAnchors.set(/** @type {HTMLElement} */ (target), el);
434
+ anchor.parentNode?.insertBefore(rootNode, anchor.nextSibling);
435
+ afterAnchors.set(/** @type {HTMLElement} */ (target), rootNode);
331
436
 
332
437
  } else {
333
- target.appendChild(el);
438
+ target.appendChild(rootNode);
334
439
  }
335
440
  }
336
441
 
@@ -338,10 +443,6 @@ export function domBuilder(/** @type {Array<DomBuilderItem|string|Node>} */ stru
338
443
  if (safeItem.callback && typeof safeItem.callback === 'function') {
339
444
  safeItem.callback(el);
340
445
  }
341
-
342
- if (grand_parent) {
343
- target = grand_parent;
344
- }
345
446
  }
346
447
 
347
448
  });
@@ -34,7 +34,7 @@ const boolAttrs = new Set([
34
34
 
35
35
  /**
36
36
  * Applica a un elemento la configurazione di base di domBuilder: classi, id e attributi.
37
- * @param {HTMLElement} element - L'elemento da configurare.
37
+ * @param {HTMLElement} element - L'elemento da configurare (anche SVG o MathML).
38
38
  * @param {DomBuilderItem} domBuilderItem - L'oggetto di configurazione di domBuilder.
39
39
  * @returns {HTMLElement}
40
40
  */
@@ -64,13 +64,14 @@ export function domBuilderBasicSetup (element, domBuilderItem) {
64
64
  });
65
65
 
66
66
  // assegna classi e id dopo attrs così che le proprietà a livello di oggetto abbiano la precedenza
67
+ // setAttribute invece di `className`/`id`: sugli elementi SVG `className` è un SVGAnimatedString di sola lettura
67
68
  if(domBuilderItem.className) {
68
- element.className = Array.isArray(domBuilderItem.className)
69
+ element.setAttribute('class', Array.isArray(domBuilderItem.className)
69
70
  ? domBuilderItem.className.filter(Boolean).join(' ')
70
- : domBuilderItem.className;
71
+ : domBuilderItem.className);
71
72
  }
72
73
  if(domBuilderItem.id) {
73
- element.id = domBuilderItem.id;
74
+ element.setAttribute('id', domBuilderItem.id);
74
75
  }
75
76
 
76
77
  return element;
@@ -14,7 +14,7 @@ const DOM_STRING_REGEX = new RegExp(
14
14
  * `tag#id.class1.class2.classN(attr1: val1, attr2=val2) text content`
15
15
  *
16
16
  * Dove:
17
- * * `tag` è il nome dell'elemento; se omesso vale `div`
17
+ * * `tag` è il nome dell'elemento, con la grafia originale (es. `linearGradient`); se omesso vale `div`
18
18
  * * `#id` è l'id opzionale dell'elemento, preceduto da `#`
19
19
  * * `.class1, .class2...` sono classi CSS opzionali, ciascuna preceduta da `.`
20
20
  * * i token `#id` e `.class` possono comparire in qualsiasi ordine e alternati (es. `.class1#id.class2`)
@@ -52,7 +52,7 @@ export function parseDomString(domString) {
52
52
  idMatch = idAndClasses.match(/#([a-zA-Z0-9_-]+)/),
53
53
  classMatches = [...idAndClasses.matchAll(/\.([a-zA-Z0-9_-]+)/g)];
54
54
 
55
- const tag = (matches[1] || 'div').toLowerCase(),
55
+ const tag = matches[1] || 'div', // grafia conservata: createElementNS distingue maiuscole e minuscole (linearGradient, clipPath, ...)
56
56
  id = idMatch?.[1] || null,
57
57
  classes = classMatches.map(m => m[1]),
58
58
  rawAttrs = matches[3]?.trim() || null,
@@ -0,0 +1,147 @@
1
+ /** URI dei namespace supportati tramite alias da domBuilder */
2
+ export const NS = {
3
+ html: 'http://www.w3.org/1999/xhtml',
4
+ svg: 'http://www.w3.org/2000/svg',
5
+ math: 'http://www.w3.org/1998/Math/MathML'
6
+ };
7
+
8
+ // URI assoluto generico (schema seguito da `:`), per namespace diversi dai tre alias
9
+ const URI_REGEX = /^[a-z][a-z0-9+.-]*:\S+$/i;
10
+
11
+ // elementi MathML i cui figli tornano nel namespace HTML (text integration point)
12
+ const MATHML_TEXT_INTEGRATION_POINTS = new Set(['mi', 'mo', 'mn', 'ms', 'mtext']);
13
+
14
+ // eccezioni: dentro i text integration point MathML questi elementi restano MathML
15
+ const MATHML_TEXT_INTEGRATION_EXCEPTIONS = new Set(['mglyph', 'malignmark']);
16
+
17
+ // valori di `encoding` che rendono `annotation-xml` un punto di ritorno all'HTML
18
+ const ANNOTATION_XML_HTML_ENCODINGS = new Set(['text/html', 'application/xhtml+xml']);
19
+
20
+ /**
21
+ * Normalizza un valore di namespace (alias o URI completo).
22
+ *
23
+ * @param {string | null | undefined} value - Alias (`'html'`, `'svg'`, `'math'`) o URI completo
24
+ * @param {string} [source='namespace'] - Nome della proprietà da citare nel messaggio d'errore (default: 'namespace')
25
+ * @returns {string | null} L'URI del namespace, oppure null se il valore è assente o non valido
26
+ * (in quest'ultimo caso viene scritto un `console.error`)
27
+ *
28
+ * @example
29
+ * normalizeNamespace('svg'); // 'http://www.w3.org/2000/svg'
30
+ * normalizeNamespace('http://www.w3.org/2000/svg'); // 'http://www.w3.org/2000/svg'
31
+ * normalizeNamespace(undefined); // null
32
+ * normalizeNamespace('foo', 'item.namespace'); // null + console.error
33
+ */
34
+ export function normalizeNamespace(value, source = 'namespace') {
35
+ if (value == null) {
36
+ return null;
37
+ }
38
+
39
+ if (typeof value === 'string') {
40
+ if (Object.hasOwn(NS, value)) {
41
+ return NS[/** @type {keyof typeof NS} */ (value)];
42
+ }
43
+ if (URI_REGEX.test(value)) {
44
+ return value;
45
+ }
46
+ }
47
+
48
+ // eslint-disable-next-line no-console
49
+ console.error(`[domBuilder]: \`${source}\` non valido (accettati 'html', 'svg', 'math' o un URI completo) → ` + value);
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * Determina il namespace in cui creare un elemento.
55
+ *
56
+ * Ordine di risoluzione:
57
+ * 1. `explicitNs` (da `item.namespace`), se presente;
58
+ * 2. `svg` → SVG e `math` → MathML, qualunque sia il genitore;
59
+ * 3. punti di ritorno all'HTML: figli di `foreignObject` (SVG), di `mi`/`mo`/`mn`/`ms`/`mtext` (MathML,
60
+ * tranne `mglyph` e `malignmark`) e di `annotation-xml` con `encoding` `text/html` o `application/xhtml+xml`;
61
+ * 4. namespace del genitore effettivo;
62
+ * 5. `fallbackNs` (da `options.namespace`), quando il genitore manca o non ha namespace;
63
+ * 6. HTML.
64
+ *
65
+ * @param {string} tag - Nome del tag
66
+ * @param {Node | null} [effectiveParent] - Genitore effettivo dell'elemento (default: null)
67
+ * @param {string | null} [explicitNs] - URI già normalizzato da `item.namespace` (default: null)
68
+ * @param {string | null} [fallbackNs] - URI già normalizzato da `options.namespace` (default: null)
69
+ * @returns {string} L'URI del namespace
70
+ *
71
+ * @example
72
+ * resolveNamespace('svg'); // NS.svg
73
+ * resolveNamespace('use', svgElement); // NS.svg
74
+ * resolveNamespace('div', foreignObjectElement); // NS.html
75
+ * resolveNamespace('use', null, null, NS.svg); // NS.svg
76
+ * resolveNamespace('div'); // NS.html
77
+ */
78
+ export function resolveNamespace(tag, effectiveParent = null, explicitNs = null, fallbackNs = null) {
79
+
80
+ if (explicitNs) {
81
+ return explicitNs;
82
+ }
83
+
84
+ const lowerTag = tag.toLowerCase();
85
+ if (lowerTag === 'svg') {
86
+ return NS.svg;
87
+ }
88
+ if (lowerTag === 'math') {
89
+ return NS.math;
90
+ }
91
+
92
+ const parentEl = effectiveParent instanceof Element ? effectiveParent : null;
93
+
94
+ if (parentEl) {
95
+ const parentNs = parentEl.namespaceURI;
96
+ const parentName = parentEl.localName;
97
+
98
+ if (parentNs === NS.svg && parentName === 'foreignObject') {
99
+ return NS.html;
100
+ }
101
+
102
+ if (parentNs === NS.math) {
103
+ if (MATHML_TEXT_INTEGRATION_POINTS.has(parentName) && !MATHML_TEXT_INTEGRATION_EXCEPTIONS.has(tag)) {
104
+ return NS.html;
105
+ }
106
+ if (parentName === 'annotation-xml'
107
+ && ANNOTATION_XML_HTML_ENCODINGS.has((parentEl.getAttribute('encoding') ?? '').toLowerCase())) {
108
+ return NS.html;
109
+ }
110
+ }
111
+
112
+ if (parentNs) {
113
+ return parentNs;
114
+ }
115
+ }
116
+
117
+ return fallbackNs ?? NS.html;
118
+ }
119
+
120
+ /**
121
+ * Crea un elemento nel namespace risolto da `resolveNamespace`.
122
+ * Per l'HTML usa `document.createElement` (comportamento storico di domBuilder), per gli altri namespace
123
+ * `document.createElementNS`, che distingue maiuscole e minuscole (`linearGradient`, `clipPath`, ...).
124
+ *
125
+ * @param {string} tag - Nome del tag
126
+ * @param {Node | null} [effectiveParent] - Genitore effettivo dell'elemento (default: null)
127
+ * @param {string | null} [explicitNs] - URI già normalizzato da `item.namespace` (default: null)
128
+ * @param {string | null} [fallbackNs] - URI già normalizzato da `options.namespace` (default: null)
129
+ * @returns {Element}
130
+ *
131
+ * @example
132
+ * createDomElement('div'); // HTMLDivElement
133
+ * createDomElement('svg'); // SVGSVGElement
134
+ * createDomElement('linearGradient', svgEl); // SVGLinearGradientElement
135
+ * createDomElement('use', null, NS.svg); // SVGUseElement
136
+ */
137
+ export function createDomElement(tag, effectiveParent = null, explicitNs = null, fallbackNs = null) {
138
+ const ns = resolveNamespace(tag, effectiveParent, explicitNs, fallbackNs);
139
+
140
+ if (ns === NS.html) {
141
+ return document.createElement(tag);
142
+ }
143
+
144
+ // `svg` e `math` vanno scritti in minuscolo per essere riconosciuti nei rispettivi namespace
145
+ const lowerTag = tag.toLowerCase();
146
+ return document.createElementNS(ns, lowerTag === 'svg' || lowerTag === 'math' ? lowerTag : tag);
147
+ }