@massimo-cassandro/minimo 1.45.0 → 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.
- package/dev-tools/custom-media-builder/custom-media-builder.mjs +7 -3
- package/dev-tools/starter-kit/source_files/_private/sf.md +2 -1
- package/dev-tools/starter-kit/source_files/root/_gitignore +3 -0
- package/dev-tools/starter-kit/source_files/root/_symfony.local.yaml +29 -0
- package/dev-tools/starter-kit/source_files/root/eslint.config.mjs +8 -16
- package/dev-tools/starter-kit/source_files/root/webpack.config-tpl.mjs +2 -2
- package/package.json +13 -13
- package/src/utilities/dom-builder/dom-builder.js +123 -22
- package/src/utilities/dom-builder/domBuilderBasicSetup.js +5 -4
- package/src/utilities/dom-builder/parseDomString.js +2 -2
- package/src/utilities/dom-builder/resolveNamespace.js +147 -0
|
@@ -5,6 +5,7 @@ import fs from 'fs';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import process from 'process';
|
|
7
7
|
import { pathToFileURL } from 'url';
|
|
8
|
+
import { styleText } from 'util';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Genera il file css delle @custom-media.
|
|
@@ -30,6 +31,7 @@ export async function buildCustomMedia({
|
|
|
30
31
|
const srcPath = path.resolve(src),
|
|
31
32
|
destPath = path.resolve(dest ?? srcPath.replace(/\.m?js$/, '.css'));
|
|
32
33
|
|
|
34
|
+
|
|
33
35
|
const { default: customMedia } = await import(pathToFileURL(srcPath).href);
|
|
34
36
|
|
|
35
37
|
if (!customMedia || typeof customMedia !== 'object') {
|
|
@@ -44,7 +46,7 @@ export async function buildCustomMedia({
|
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
const content = '/* File generato da custom-media-builder.mjs a partire da ' +
|
|
47
|
-
`${path.basename(srcPath)}
|
|
49
|
+
`${path.basename(srcPath)}.\nEVENTUALI MODIFICHE ANDRANNO PERDUTE */\n\n` +
|
|
48
50
|
rows.join('\n') + '\n';
|
|
49
51
|
|
|
50
52
|
if (fs.existsSync(destPath) && fs.readFileSync(destPath, 'utf8') === content) {
|
|
@@ -56,7 +58,7 @@ export async function buildCustomMedia({
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
// esecuzione da riga di comando
|
|
59
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(
|
|
61
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) {
|
|
60
62
|
const args = Object.fromEntries(
|
|
61
63
|
process.argv.slice(2)
|
|
62
64
|
.map(arg => arg.match(/^--(src|dest)=(.+)$/))
|
|
@@ -66,7 +68,9 @@ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.ar
|
|
|
66
68
|
|
|
67
69
|
try {
|
|
68
70
|
const written = await buildCustomMedia(args);
|
|
69
|
-
|
|
71
|
+
|
|
72
|
+
console.log(styleText('yellow', written ? 'custom media: file aggiornato' : 'custom media: nessuna modifica'));
|
|
73
|
+
|
|
70
74
|
} catch (err) {
|
|
71
75
|
console.error(err.message);
|
|
72
76
|
process.exit(1);
|
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
//
|
|
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'), //
|
|
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.
|
|
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.
|
|
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.
|
|
108
|
-
"jsdom": "^30.
|
|
109
|
-
"style-dictionary": "^5.
|
|
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.
|
|
113
|
-
"@inquirer/select": "^5.2.
|
|
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.
|
|
115
|
+
"@stylistic/stylelint-plugin": "^5.3.1",
|
|
116
116
|
"clipboardy": "^5.3.2",
|
|
117
117
|
"json5": "^2.2.3",
|
|
118
|
-
"sharp": "^0.35.
|
|
118
|
+
"sharp": "^0.35.5",
|
|
119
119
|
"sharp-ico": "^0.1.5",
|
|
120
|
-
"stylelint": "^17.
|
|
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.
|
|
126
|
-
"stylelint-scss": "^7.
|
|
127
|
-
"svgo": "^4.0
|
|
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
|
|
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:
|
|
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 {
|
|
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
|
-
* @
|
|
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 {
|
|
199
|
+
/** @type {Element | DocumentFragment | null} */
|
|
133
200
|
let target = useFragment ? document.createDocumentFragment() : (parent ?? null);
|
|
134
201
|
|
|
135
|
-
|
|
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
|
-
|
|
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 (
|
|
243
|
-
|
|
334
|
+
if (chainParent) {
|
|
335
|
+
chainParent.appendChild(el);
|
|
336
|
+
} else {
|
|
337
|
+
chainRoot = el;
|
|
244
338
|
}
|
|
245
|
-
|
|
339
|
+
chainParent = el;
|
|
246
340
|
}
|
|
247
341
|
|
|
248
342
|
});
|
|
249
343
|
|
|
250
344
|
} else {
|
|
251
345
|
el = domBuilderBasicSetup(
|
|
252
|
-
|
|
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(
|
|
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(
|
|
330
|
-
afterAnchors.set(/** @type {HTMLElement} */ (target),
|
|
434
|
+
anchor.parentNode?.insertBefore(rootNode, anchor.nextSibling);
|
|
435
|
+
afterAnchors.set(/** @type {HTMLElement} */ (target), rootNode);
|
|
331
436
|
|
|
332
437
|
} else {
|
|
333
|
-
target.appendChild(
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
+
}
|