mypgs 1.6.0 → 2.0.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/AGENTS.md +2 -2
- package/assets/javascript/components/_accordion.js +2 -2
- package/assets/javascript/components/_modals.js +159 -136
- package/assets/javascript/components/_notifications.js +19 -9
- package/assets/javascript/components/_search.js +2 -2
- package/assets/javascript/components/_slides.js +66 -32
- package/assets/javascript/components/_stepTabs.js +4 -4
- package/assets/javascript/components/_steps.js +2 -2
- package/assets/javascript/components/_summary.js +2 -2
- package/assets/javascript/functions/_formValidate.js +97 -53
- package/assets/javascript/functions/_scrollY.js +10 -10
- package/assets/javascript/layout/_header.js +3 -0
- package/assets/scss/base/_html.scss +2 -2
- package/assets/scss/base/_variables.scss +2 -2
- package/assets/scss/components/_form.scss +2 -2
- package/assets/scss/components/_notification.scss +23 -30
- package/assets/scss/components/_search.scss +27 -21
- package/assets/scss/layout/_pageShell.scss +18 -15
- package/assets/scss/mixin/_mx-form.scss +7 -7
- package/{templates/html → demo}/demo.html +1 -1
- package/demo/demo.js +240 -0
- package/dist/css/index.css +86 -92
- package/dist/css/index.css.map +1 -1
- package/dist/css/index.min.css +1 -1
- package/dist/javascript/index.js +356 -255
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/index.min.js +1 -1
- package/docs/componenti-e-markup.md +26 -27
- package/docs/components/accordion.md +61 -0
- package/docs/components/badges.md +67 -0
- package/docs/components/breadcumbs.md +43 -0
- package/docs/components/button.md +59 -0
- package/docs/components/card.md +50 -0
- package/docs/components/dropdown.md +135 -0
- package/docs/components/form.md +113 -0
- package/docs/components/logo.md +36 -0
- package/docs/components/menu.md +90 -0
- package/docs/components/modal.md +131 -0
- package/docs/components/notification.md +81 -0
- package/docs/components/search.md +117 -0
- package/docs/components/slides.md +85 -0
- package/docs/components/stepTabs.md +93 -0
- package/docs/components/steps.md +56 -0
- package/docs/components/summary.md +49 -0
- package/docs/components/table.md +42 -0
- package/docs/components/tooltip.md +53 -0
- package/docs/helper-javascript.md +68 -3
- package/docs/layout/body.md +41 -0
- package/docs/layout/flex.md +116 -0
- package/docs/layout/footer.md +71 -0
- package/docs/layout/grid.md +116 -0
- package/docs/layout/header.md +116 -0
- package/docs/layout/pageShell.md +94 -0
- package/docs/layout/section.md +69 -0
- package/docs/patterns/cookieConsent.md +89 -0
- package/package.json +2 -1
- package/scripts/generate-component-docs.js +557 -0
- package/templates/html/components/accordion.html +33 -4
- package/templates/html/components/badges.html +23 -0
- package/templates/html/components/breadcumbs.html +14 -0
- package/templates/html/components/button.html +22 -0
- package/templates/html/components/card.html +20 -0
- package/templates/html/components/dropdown.html +34 -0
- package/templates/html/components/form.html +82 -4
- package/templates/html/components/logo.html +21 -0
- package/templates/html/components/menu.html +33 -6
- package/templates/html/components/modal.html +44 -0
- package/templates/html/components/notification.html +68 -32
- package/templates/html/components/search.html +76 -0
- package/templates/html/components/slides.html +43 -0
- package/templates/html/components/stepTabs.html +47 -0
- package/templates/html/components/steps.html +24 -0
- package/templates/html/components/summary.html +27 -0
- package/templates/html/components/table.html +12 -0
- package/templates/html/components/tooltip.html +35 -0
- package/templates/html/layout/body.html +20 -5
- package/templates/html/layout/flex.html +20 -0
- package/templates/html/layout/footer.html +29 -1
- package/templates/html/layout/grid.html +20 -0
- package/templates/html/layout/header.html +41 -0
- package/templates/html/layout/pageShell.html +19 -0
- package/templates/html/layout/section.html +32 -10
- package/templates/html/patterns/cookieConsent.html +31 -0
- package/templates/react/components/form.jsx +3 -3
- package/templates/react/components/notification.jsx +4 -4
- package/templates/react/patterns/footer.jsx +1 -1
- package/templates/html/demo.js +0 -130
- /package/{templates/html → demo}/demo.css +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<!-- File generato automaticamente da templates/html/patterns/cookieConsent.html. Modificare templates/html/patterns/cookieConsent.html e rieseguire npm run docs:generate. -->
|
|
2
|
+
|
|
3
|
+
# Cookie Consent
|
|
4
|
+
|
|
5
|
+
Pattern accessibile per raccogliere e memorizzare il consenso ai cookie analitici, applicare le preferenze e caricare Google Analytics solo quando autorizzato.
|
|
6
|
+
|
|
7
|
+
## PGS
|
|
8
|
+
|
|
9
|
+
- `cookieConsent`: identifica il pannello principale inizializzato dal pattern JavaScript.
|
|
10
|
+
- `cookieConsent-panel`: identifica il gruppo delle preferenze configurabili.
|
|
11
|
+
- `cookieConsent-featureEssential`: identifica la riga dei cookie tecnici sempre attivi.
|
|
12
|
+
- `cookieConsent-panel-badge`: identifica l'indicatore dello stato dei cookie tecnici.
|
|
13
|
+
- `cookieConsent-featureAnalytics`: identifica la riga dei cookie analitici opzionali.
|
|
14
|
+
- `cookieConsent-toggleAnalytics`: identifica il controllo che abilita il consenso analytics.
|
|
15
|
+
- `cookieConsent-actionReject`: applica le preferenze selezionate senza accettare automaticamente analytics.
|
|
16
|
+
- `cookieConsent-actionAccept`: accetta tutti i cookie disponibili.
|
|
17
|
+
- `cookieConsent-actionOpen`: identifica controlli esterni che riaprono il pannello delle preferenze.
|
|
18
|
+
- `cookieConsent-gaUnavailable`: token runtime applicato al root quando manca un measurement ID utilizzabile.
|
|
19
|
+
- `cookieConsent-accepted`: token runtime applicato al root quando analytics è consentito.
|
|
20
|
+
- `cookieConsent-declined`: token runtime applicato al root quando analytics non è consentito.
|
|
21
|
+
|
|
22
|
+
## Elementi correlati
|
|
23
|
+
|
|
24
|
+
- `flexColumn`: organizza verticalmente il pannello delle preferenze.
|
|
25
|
+
- `flexRow`: dispone righe e azioni del pannello.
|
|
26
|
+
- `nowrap`: impedisce il ritorno a capo nella riga dei cookie essenziali.
|
|
27
|
+
- `toggle`: presenta il consenso analytics come interruttore.
|
|
28
|
+
- `button`: presenta l'azione di selezione con lo stile standard.
|
|
29
|
+
- `buttonStrong`: presenta l'accettazione completa come azione primaria.
|
|
30
|
+
|
|
31
|
+
## Output
|
|
32
|
+
|
|
33
|
+
Pannello HTML di consenso con preferenze tecniche e analytics, azioni e attributi accessibili.
|
|
34
|
+
|
|
35
|
+
## Esempio
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<section id="pgs-cookieConsent" pgs="cookieConsent" role="dialog" aria-modal="true" data-ga-id="G-XXXXXXXXXX" hidden tabindex="-1">
|
|
39
|
+
<p><i class="fa-duotone fa-solid fa-cookie-bite"></i> Cookie e privacy <br></p>
|
|
40
|
+
<h2>La tua privacy prima di tutto</h2>
|
|
41
|
+
|
|
42
|
+
<p>
|
|
43
|
+
Utilizziamo cookie tecnici per offrire il servizio e, previo consenso, cookie analitici di
|
|
44
|
+
<strong>Google Analytics</strong> per misurare in modo anonimo il traffico e migliorare i contenuti.
|
|
45
|
+
Puoi modificare la scelta in qualsiasi momento.
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
<p>
|
|
49
|
+
<a href="/privacy-policy/" target="_blank" rel="noopener">Privacy Policy</a> -
|
|
50
|
+
<a href="/cookie-policy/" target="_blank" rel="noopener">Cookie Policy</a>
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<div pgs="cookieConsent-panel flexColumn" role="group" aria-label="Preferenze cookie">
|
|
54
|
+
<div pgs="flexRow nowrap cookieConsent-featureEssential">
|
|
55
|
+
<div>
|
|
56
|
+
<p>
|
|
57
|
+
<strong>Cookie tecnici</strong>
|
|
58
|
+
<br>
|
|
59
|
+
<small>Sempre attivi per garantire il corretto funzionamento del sito.</small>
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<span pgs="cookieConsent-panel-badge">Attivi</span>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div pgs="flexRow cookieConsent-featureAnalytics">
|
|
67
|
+
<label pgs="toggle">
|
|
68
|
+
<p>
|
|
69
|
+
<strong>Analytics</strong>
|
|
70
|
+
<br>
|
|
71
|
+
<small>Dati di navigazione raccolti in forma aggregata per statistiche anonime.</small>
|
|
72
|
+
</p>
|
|
73
|
+
|
|
74
|
+
<input type="checkbox" pgs="cookieConsent-toggleAnalytics" aria-label="Abilita Google Analytics">
|
|
75
|
+
</label>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div pgs="flexRow">
|
|
80
|
+
<button type="button" pgs="button cookieConsent-actionReject">
|
|
81
|
+
<i class="fa-solid fa-duotone fa-sliders"></i>Solo selezionati
|
|
82
|
+
</button>
|
|
83
|
+
|
|
84
|
+
<button type="button" pgs="buttonStrong cookieConsent-actionAccept">
|
|
85
|
+
<i class="fa-solid fa-check"></i> Accetta tutto
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mypgs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "MyPGS frontend theme and UI behaviors, with bundled JavaScript and CSS assets.",
|
|
5
5
|
"main": "./dist/javascript/index.js",
|
|
6
6
|
"module": "./assets/javascript/index.js",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"style": "./dist/css/index.css",
|
|
23
23
|
"scripts": {
|
|
24
|
+
"docs:generate": "node scripts/generate-component-docs.js",
|
|
24
25
|
"start": "webpack",
|
|
25
26
|
"start watch": "webpack --watch",
|
|
26
27
|
"syncmaster-forzato": "git fetch origin && git checkout dev && git pull --rebase origin dev && git rebase origin/master && git push --force-with-lease origin dev && git checkout master && git pull --ff-only origin master && git merge --ff-only dev && git push origin master && git checkout dev",
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
9
|
+
const TEMPLATE_ROOT = path.join(PROJECT_ROOT, "templates", "html");
|
|
10
|
+
const DOCS_ROOT = path.join(PROJECT_ROOT, "docs");
|
|
11
|
+
const MANAGED_DOC_DIRECTORIES = ["components", "layout", "patterns"]
|
|
12
|
+
.map(directory => path.join(DOCS_ROOT, directory));
|
|
13
|
+
const SOURCE_ROOTS = [
|
|
14
|
+
path.join(PROJECT_ROOT, "assets", "javascript"),
|
|
15
|
+
path.join(PROJECT_ROOT, "assets", "scss"),
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const TAG_ORDER = ["title", "description", "pgs", "pgs-option", "pgs-state", "api", "related", "return"];
|
|
19
|
+
const LIST_TAGS = new Set(["pgs", "pgs-option", "pgs-state", "api", "related"]);
|
|
20
|
+
const REQUIRED_TAGS = ["title", "description", "pgs"];
|
|
21
|
+
const GENERATED_MARKER = /^<!-- File generato automaticamente da (templates\/html\/.+\.html)\. Modificare \1 e rieseguire npm run docs:generate\. -->$/;
|
|
22
|
+
|
|
23
|
+
function toPosix(value) {
|
|
24
|
+
return value.split(path.sep).join("/");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function relativeToProject(value) {
|
|
28
|
+
return toPosix(path.relative(PROJECT_ROOT, value));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getOutputPath(template) {
|
|
32
|
+
const relativeTemplate = path.relative(TEMPLATE_ROOT, template);
|
|
33
|
+
return path.join(DOCS_ROOT, relativeTemplate.replace(/\.html$/i, ".md"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeEol(value) {
|
|
37
|
+
return value.replace(/\r\n?/g, "\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function walkFiles(root, predicate) {
|
|
41
|
+
if (!fs.existsSync(root)) return [];
|
|
42
|
+
|
|
43
|
+
const files = [];
|
|
44
|
+
const visit = directory => {
|
|
45
|
+
fs.readdirSync(directory, { withFileTypes: true })
|
|
46
|
+
.sort((a, b) => a.name.localeCompare(b.name, "en"))
|
|
47
|
+
.forEach(entry => {
|
|
48
|
+
const absolute = path.join(directory, entry.name);
|
|
49
|
+
if (entry.isDirectory()) visit(absolute);
|
|
50
|
+
else if (entry.isFile() && predicate(absolute)) files.push(absolute);
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
visit(root);
|
|
55
|
+
return files.sort((a, b) => toPosix(a).localeCompare(toPosix(b), "en"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createError(file, message, suggestion, section, value) {
|
|
59
|
+
return { file, message, suggestion, section, value };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseDocumentationBlock(file, source) {
|
|
63
|
+
const errors = [];
|
|
64
|
+
const relativeFile = relativeToProject(file);
|
|
65
|
+
const hasBom = source.charCodeAt(0) === 0xFEFF;
|
|
66
|
+
const content = hasBom ? source.slice(1) : source;
|
|
67
|
+
const blockPattern = /\/\*\*[\s\S]*?\*\//g;
|
|
68
|
+
const taggedBlocks = [...content.matchAll(blockPattern)]
|
|
69
|
+
.filter(match => /@(title|description|pgs(?:-option|-state)?|api|related|return)\b/.test(match[0]));
|
|
70
|
+
const initialHtmlComment = content.match(/^<!--[\t\r\n ]*(\/\*\*[\s\S]*?\*\/)[\t\r\n ]*-->/);
|
|
71
|
+
|
|
72
|
+
if (taggedBlocks.length === 0) {
|
|
73
|
+
errors.push(createError(relativeFile, "Commento strutturato iniziale mancante.", "Aggiungi un blocco <!-- /** ... */ --> come primo contenuto del file."));
|
|
74
|
+
return { errors };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (taggedBlocks.length > 1) {
|
|
78
|
+
errors.push(createError(relativeFile, `Trovati ${taggedBlocks.length} commenti strutturati.`, "Mantieni un solo blocco di documentazione iniziale."));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const first = taggedBlocks[0];
|
|
82
|
+
const hasValidHtmlWrapper = initialHtmlComment && initialHtmlComment[1] === first[0];
|
|
83
|
+
if (!hasValidHtmlWrapper) {
|
|
84
|
+
errors.push(createError(
|
|
85
|
+
relativeFile,
|
|
86
|
+
"Il commento strutturato non è un commento HTML iniziale valido.",
|
|
87
|
+
"Posizionalo all'inizio e racchiudi il blocco /** ... */ tra <!-- e -->, così il browser non lo mostra nella pagina.",
|
|
88
|
+
));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lines = normalizeEol(first[0]).split("\n");
|
|
92
|
+
if (lines[0].trim() !== "/**" || lines.at(-1).trim() !== "*/") {
|
|
93
|
+
errors.push(createError(relativeFile, "Delimitatori del commento strutturato non validi.", "Usa /** su una riga e */ su una riga separata."));
|
|
94
|
+
return { errors };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const bodyLines = [];
|
|
98
|
+
for (let index = 1; index < lines.length - 1; index += 1) {
|
|
99
|
+
const match = lines[index].match(/^\s*\*(?: ?(.*))?$/);
|
|
100
|
+
if (!match) {
|
|
101
|
+
errors.push(createError(relativeFile, `Riga ${index + 1} del commento non valida.`, "Prefissa ogni riga interna con *.", "commento"));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
bodyLines.push(match[1] || "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const data = {
|
|
108
|
+
title: "",
|
|
109
|
+
description: "",
|
|
110
|
+
pgs: [],
|
|
111
|
+
"pgs-option": [],
|
|
112
|
+
"pgs-state": [],
|
|
113
|
+
api: [],
|
|
114
|
+
related: [],
|
|
115
|
+
return: "",
|
|
116
|
+
};
|
|
117
|
+
const seenTags = new Set();
|
|
118
|
+
let activeTag = null;
|
|
119
|
+
let previousOrder = -1;
|
|
120
|
+
|
|
121
|
+
bodyLines.forEach((rawLine, lineIndex) => {
|
|
122
|
+
const line = rawLine.trim();
|
|
123
|
+
if (!line) return;
|
|
124
|
+
|
|
125
|
+
const tagMatch = line.match(/^@([a-z-]+)(?:\s+(.*))?$/);
|
|
126
|
+
if (tagMatch) {
|
|
127
|
+
const tag = tagMatch[1];
|
|
128
|
+
const value = (tagMatch[2] || "").trim();
|
|
129
|
+
const order = TAG_ORDER.indexOf(tag);
|
|
130
|
+
|
|
131
|
+
if (order === -1) {
|
|
132
|
+
errors.push(createError(relativeFile, `Tag sconosciuto @${tag}.`, "Usa solamente i tag previsti dal formato.", `@${tag}`));
|
|
133
|
+
activeTag = null;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (seenTags.has(tag)) {
|
|
137
|
+
errors.push(createError(relativeFile, `Sezione @${tag} duplicata.`, "Mantieni una sola sezione per tag.", `@${tag}`));
|
|
138
|
+
}
|
|
139
|
+
if (order < previousOrder) {
|
|
140
|
+
errors.push(createError(relativeFile, `La sezione @${tag} non rispetta l'ordine canonico.`, `Usa l'ordine: ${TAG_ORDER.map(item => `@${item}`).join(", ")}.`, `@${tag}`));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
seenTags.add(tag);
|
|
144
|
+
previousOrder = Math.max(previousOrder, order);
|
|
145
|
+
activeTag = tag;
|
|
146
|
+
|
|
147
|
+
if (LIST_TAGS.has(tag)) {
|
|
148
|
+
if (value) errors.push(createError(relativeFile, `La sezione @${tag} deve iniziare su una riga senza contenuto.`, "Sposta le voci nelle righe successive con il formato - valore: descrizione.", `@${tag}`));
|
|
149
|
+
} else {
|
|
150
|
+
if (!value) errors.push(createError(relativeFile, `Il tag @${tag} è vuoto.`, "Aggiungi il testo sulla stessa riga del tag.", `@${tag}`));
|
|
151
|
+
data[tag] = value;
|
|
152
|
+
activeTag = null;
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!activeTag || !LIST_TAGS.has(activeTag)) {
|
|
158
|
+
errors.push(createError(relativeFile, `Contenuto non associato a un tag alla riga ${lineIndex + 2}.`, "Inserisci il contenuto in una sezione riconosciuta."));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const itemMatch = line.match(/^-\s+([^:]+):\s+(.+)$/);
|
|
163
|
+
if (!itemMatch) {
|
|
164
|
+
errors.push(createError(relativeFile, `Voce non valida nella sezione @${activeTag}: "${line}".`, "Usa il formato - valore: descrizione.", `@${activeTag}`));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const key = itemMatch[1].trim();
|
|
169
|
+
const description = itemMatch[2].trim();
|
|
170
|
+
if (!key || !description) {
|
|
171
|
+
errors.push(createError(relativeFile, `Voce incompleta nella sezione @${activeTag}.`, "Specifica sia il valore sia la descrizione.", `@${activeTag}`, key));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (data[activeTag].some(item => item.key === key)) {
|
|
175
|
+
errors.push(createError(relativeFile, `Valore duplicato "${key}" nella sezione @${activeTag}.`, "Rimuovi la voce duplicata.", `@${activeTag}`, key));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (activeTag === "pgs-option" && /[\s[\]]/.test(key)) {
|
|
179
|
+
errors.push(createError(relativeFile, `La voce @pgs-option "${key}" non è una chiave valida.`, "Documenta la chiave senza payload, per esempio position invece di position[top left].", "@pgs-option", key));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
data[activeTag].push({ key, description });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
REQUIRED_TAGS.forEach(tag => {
|
|
186
|
+
const value = data[tag];
|
|
187
|
+
if (!seenTags.has(tag) || (Array.isArray(value) ? value.length === 0 : !value)) {
|
|
188
|
+
errors.push(createError(relativeFile, `Sezione obbligatoria @${tag} mancante o vuota.`, `Aggiungi una sezione @${tag} valida.`, `@${tag}`));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
LIST_TAGS.forEach(tag => {
|
|
193
|
+
if (seenTags.has(tag) && data[tag].length === 0) {
|
|
194
|
+
errors.push(createError(relativeFile, `Sezione @${tag} dichiarata ma vuota.`, `Aggiungi almeno una voce oppure rimuovi la sezione @${tag}.`, `@${tag}`));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const documentationContainer = hasValidHtmlWrapper ? initialHtmlComment[0] : first[0];
|
|
199
|
+
const markupStart = hasValidHtmlWrapper ? documentationContainer.length : first.index + documentationContainer.length;
|
|
200
|
+
const markup = normalizeEol(content.slice(markupStart)).trim();
|
|
201
|
+
if (!markup) {
|
|
202
|
+
errors.push(createError(relativeFile, "Contenuto HTML mancante sotto il commento.", "Mantieni l'esempio HTML ufficiale dopo il blocco di documentazione."));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { errors, data, markup, block: documentationContainer, hasBom };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function tokenizeOptions(value) {
|
|
209
|
+
return value.match(/[^\s[\]]+(?:\[[^\]]*\])?/g) || [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function splitOption(value) {
|
|
213
|
+
const match = value.match(/^([^\s[\]]+)(?:\[([^\]]*)\])?$/);
|
|
214
|
+
return match ? { key: match[1], payload: match[2] } : { key: "", payload: undefined };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function extractAttributes(markup) {
|
|
218
|
+
const activeMarkup = markup.replace(/<!--[\s\S]*?-->/g, "");
|
|
219
|
+
const result = { pgs: [], options: [], states: [] };
|
|
220
|
+
const pattern = /\b(pgs(?:-option|-state)?)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
221
|
+
|
|
222
|
+
for (const match of activeMarkup.matchAll(pattern)) {
|
|
223
|
+
const attribute = match[1];
|
|
224
|
+
const value = match[3];
|
|
225
|
+
if (attribute === "pgs-option") result.options.push(...tokenizeOptions(value));
|
|
226
|
+
else if (attribute === "pgs-state") result.states.push(...value.trim().split(/\s+/).filter(Boolean));
|
|
227
|
+
else result.pgs.push(...value.trim().split(/\s+/).filter(Boolean));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
Object.keys(result).forEach(key => {
|
|
231
|
+
result[key] = [...new Set(result[key])];
|
|
232
|
+
});
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function stripSourceComments(value) {
|
|
237
|
+
return value
|
|
238
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
239
|
+
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizeName(value) {
|
|
243
|
+
return value.replace(/^_/, "").replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function singularName(value) {
|
|
247
|
+
return value.endsWith("s") ? value.slice(0, -1) : value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function escapeRegExp(value) {
|
|
251
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function containsExactToken(source, token) {
|
|
255
|
+
const escaped = escapeRegExp(token);
|
|
256
|
+
return new RegExp(`(^|[^A-Za-z0-9_-])${escaped}(?=$|[^A-Za-z0-9_-])`).test(source);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function containsPgsReference(source, token) {
|
|
260
|
+
const escaped = escapeRegExp(token);
|
|
261
|
+
const patterns = [
|
|
262
|
+
new RegExp(`\\[pgs[^\\]]*(?:["']${escaped}["']|[~=]${escaped})(?=[\\]\\s])`),
|
|
263
|
+
new RegExp(`\\.(?:querySelector|querySelectorAll|contains|add|remove|toggle)\\(\\s*["']${escaped}["']`),
|
|
264
|
+
new RegExp(`pgs\\s*=\\s*["'][^"']*(?:^|\\s)${escaped}(?=\\s|["'])`),
|
|
265
|
+
];
|
|
266
|
+
return patterns.some(pattern => pattern.test(source));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function loadSources() {
|
|
270
|
+
return SOURCE_ROOTS.flatMap(root => walkFiles(root, file => /\.(?:js|scss)$/.test(file)))
|
|
271
|
+
.map(file => ({
|
|
272
|
+
file,
|
|
273
|
+
relative: relativeToProject(file),
|
|
274
|
+
name: normalizeName(path.basename(file, path.extname(file))),
|
|
275
|
+
content: stripSourceComments(normalizeEol(fs.readFileSync(file, "utf8"))),
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function associateSources(template, documentation, sources) {
|
|
280
|
+
const basename = normalizeName(path.basename(template, ".html"));
|
|
281
|
+
const singularBasename = singularName(basename);
|
|
282
|
+
const primaryTokens = documentation.pgs.map(item => item.key);
|
|
283
|
+
|
|
284
|
+
const nameMatches = sources.filter(source => {
|
|
285
|
+
const singularSource = singularName(source.name);
|
|
286
|
+
return source.name === basename
|
|
287
|
+
|| singularSource === singularBasename
|
|
288
|
+
|| source.name.startsWith(basename)
|
|
289
|
+
|| basename.startsWith(source.name);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (nameMatches.length > 0) return nameMatches;
|
|
293
|
+
return sources.filter(source => primaryTokens.some(token => containsPgsReference(source.content, token)));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function extractSourceFacts(sources) {
|
|
297
|
+
const facts = { options: new Set(), states: new Set() };
|
|
298
|
+
|
|
299
|
+
sources.forEach(source => {
|
|
300
|
+
const content = source.content;
|
|
301
|
+
const optionPatterns = [
|
|
302
|
+
/\.option\.(?:contains|getValueBrackets|setValueBrackets)\(\s*["']([^"']+)["']\s*(?=[,)])/g,
|
|
303
|
+
/pgs-option\s*[~*^$|]?=\s*["']([^"'\[\]\s]+)(?:\[[^\]]*\])?["']/g,
|
|
304
|
+
];
|
|
305
|
+
const statePatterns = [
|
|
306
|
+
/\.state\.(?:add|remove|toggle|contains)\(\s*["']([^"']+)["']\s*(?=[,)])/g,
|
|
307
|
+
/pgs-state\s*[~*^$|]?=\s*["']([^"'\s]+)["']/g,
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
optionPatterns.forEach(pattern => {
|
|
311
|
+
for (const match of content.matchAll(pattern)) facts.options.add(splitOption(match[1]).key);
|
|
312
|
+
});
|
|
313
|
+
statePatterns.forEach(pattern => {
|
|
314
|
+
for (const match of content.matchAll(pattern)) facts.states.add(match[1]);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
facts.options.delete("");
|
|
319
|
+
facts.states.delete("");
|
|
320
|
+
return facts;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function validatePosition(file, option, errors) {
|
|
324
|
+
if (option.payload === undefined) return;
|
|
325
|
+
const parts = option.payload.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
326
|
+
const side = parts[0];
|
|
327
|
+
const align = parts[1];
|
|
328
|
+
const allowed = {
|
|
329
|
+
top: new Set(["left", "center", "right"]),
|
|
330
|
+
bottom: new Set(["left", "center", "right"]),
|
|
331
|
+
left: new Set(["top", "center", "bottom"]),
|
|
332
|
+
right: new Set(["top", "center", "bottom"]),
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
if (parts.length !== 2 || !allowed[side]?.has(align)) {
|
|
336
|
+
errors.push(createError(file, `Payload non valido per pgs-option position: "${option.payload}".`, "Usa una coppia lato/allineamento compatibile, per esempio position[bottom center].", "@pgs-option", "position"));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function validateTemplate(template, parsed, sources, allSourceContent) {
|
|
341
|
+
const errors = [...parsed.errors];
|
|
342
|
+
if (!parsed.data || !parsed.markup) return errors;
|
|
343
|
+
|
|
344
|
+
const file = relativeToProject(template);
|
|
345
|
+
const documentation = parsed.data;
|
|
346
|
+
const attributes = extractAttributes(parsed.markup);
|
|
347
|
+
const documentedPgs = new Set(documentation.pgs.map(item => item.key));
|
|
348
|
+
const documentedRelated = new Set(documentation.related.map(item => item.key));
|
|
349
|
+
const documentedOptions = new Set(documentation["pgs-option"].map(item => item.key));
|
|
350
|
+
const documentedStates = new Set(documentation["pgs-state"].map(item => item.key));
|
|
351
|
+
const associatedSources = associateSources(template, documentation, sources);
|
|
352
|
+
const associatedFacts = extractSourceFacts(associatedSources);
|
|
353
|
+
|
|
354
|
+
attributes.pgs.forEach(token => {
|
|
355
|
+
if (!documentedPgs.has(token) && !documentedRelated.has(token)) {
|
|
356
|
+
errors.push(createError(file, `Valore pgs non documentato: "${token}".`, "Aggiungilo a @pgs oppure a @related.", "@pgs", token));
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
documentedPgs.forEach(token => {
|
|
361
|
+
if (documentedRelated.has(token)) {
|
|
362
|
+
errors.push(createError(file, `Il valore "${token}" compare sia in @pgs sia in @related.`, "Documentalo in una sola sezione.", "@pgs", token));
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
[...documentedPgs, ...documentedRelated].forEach(token => {
|
|
367
|
+
if (!attributes.pgs.includes(token) && !containsExactToken(allSourceContent, token)) {
|
|
368
|
+
errors.push(createError(file, `Valore documentato non trovato nel template, JavaScript o SCSS: "${token}".`, "Correggi il nome oppure rimuovi la voce non implementata.", documentedPgs.has(token) ? "@pgs" : "@related", token));
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
attributes.options.forEach(rawOption => {
|
|
373
|
+
const option = splitOption(rawOption);
|
|
374
|
+
if (!option.key) {
|
|
375
|
+
errors.push(createError(file, `Valore pgs-option non valido: "${rawOption}".`, "Correggi la sintassi dell'attributo pgs-option.", "@pgs-option", rawOption));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!documentedOptions.has(option.key)) {
|
|
379
|
+
errors.push(createError(file, `Valore pgs-option non documentato: "${option.key}".`, "Aggiungi la chiave alla sezione @pgs-option.", "@pgs-option", option.key));
|
|
380
|
+
}
|
|
381
|
+
if (["containerID", "containerPGS", "tabIcon"].includes(option.key) && (!option.payload || !option.payload.trim())) {
|
|
382
|
+
errors.push(createError(file, `Payload mancante per pgs-option "${option.key}".`, `Usa ${option.key}[valore] con un valore non vuoto.`, "@pgs-option", option.key));
|
|
383
|
+
}
|
|
384
|
+
if (option.key === "tabIcon" && /\s/.test(option.payload || "")) {
|
|
385
|
+
errors.push(createError(file, `Il payload tabIcon deve contenere una sola classe: "${option.payload}".`, "Usa una singola classe icona, per esempio tabIcon[fa-user].", "@pgs-option", option.key));
|
|
386
|
+
}
|
|
387
|
+
if (option.key === "position") validatePosition(file, option, errors);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
documentation["pgs-option"].forEach(item => {
|
|
391
|
+
const inTemplate = attributes.options.some(value => splitOption(value).key === item.key);
|
|
392
|
+
if (!inTemplate && !associatedFacts.options.has(item.key) && !containsExactToken(allSourceContent, item.key)) {
|
|
393
|
+
errors.push(createError(file, `Il valore @pgs-option "${item.key}" non è stato trovato nei sorgenti collegati.`, "Correggi il nome o rimuovi l'opzione non implementata.", "@pgs-option", item.key));
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
associatedFacts.options.forEach(option => {
|
|
398
|
+
if (!documentedOptions.has(option)) {
|
|
399
|
+
errors.push(createError(file, `Opzione supportata ma non documentata: "${option}".`, "Aggiungila alla sezione @pgs-option.", "@pgs-option", option));
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
attributes.states.forEach(state => {
|
|
404
|
+
if (!documentedStates.has(state)) {
|
|
405
|
+
errors.push(createError(file, `Valore pgs-state non documentato: "${state}".`, "Aggiungilo alla sezione @pgs-state.", "@pgs-state", state));
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
documentation["pgs-state"].forEach(item => {
|
|
410
|
+
if (!attributes.states.includes(item.key) && !associatedFacts.states.has(item.key) && !containsExactToken(allSourceContent, item.key)) {
|
|
411
|
+
errors.push(createError(file, `Il valore @pgs-state "${item.key}" non è stato trovato nel template, JavaScript o SCSS collegato.`, "Correggi il nome o rimuovi lo stato non implementato.", "@pgs-state", item.key));
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
associatedFacts.states.forEach(state => {
|
|
416
|
+
if (!documentedStates.has(state)) {
|
|
417
|
+
errors.push(createError(file, `Stato supportato ma non documentato: "${state}".`, "Aggiungilo alla sezione @pgs-state.", "@pgs-state", state));
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
documentation.api.forEach(item => {
|
|
422
|
+
const signature = item.key.match(/^(?:new\s+)?((?:pgs|instance)(?:\.[A-Za-z_$][\w$]*)+)\([^)]*\)$/);
|
|
423
|
+
if (!signature) {
|
|
424
|
+
errors.push(createError(file, `Firma API non valida: "${item.key}".`, "Usa una firma chiamabile, per esempio pgs.modal.api(element) oppure instance.open().", "@api", item.key));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const identifiers = signature[1].split(".");
|
|
429
|
+
const implementationName = identifiers.at(-1);
|
|
430
|
+
if (!containsExactToken(allSourceContent, implementationName)) {
|
|
431
|
+
errors.push(createError(file, `API documentata non trovata nei sorgenti: "${item.key}".`, "Correggi la firma oppure rimuovi il metodo non esposto.", "@api", item.key));
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
return errors;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function renderList(items) {
|
|
439
|
+
return items.map(item => `- \`${item.key}\`: ${item.description}`).join("\n");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function renderMarkdown(template, documentation, markup) {
|
|
443
|
+
const relativeTemplate = relativeToProject(template);
|
|
444
|
+
const marker = `<!-- File generato automaticamente da ${relativeTemplate}. Modificare ${relativeTemplate} e rieseguire npm run docs:generate. -->`;
|
|
445
|
+
const sections = [marker, "", `# ${documentation.title}`, "", documentation.description];
|
|
446
|
+
const sectionMap = [
|
|
447
|
+
["PGS", documentation.pgs],
|
|
448
|
+
["PGS Options", documentation["pgs-option"]],
|
|
449
|
+
["PGS States", documentation["pgs-state"]],
|
|
450
|
+
["API JavaScript", documentation.api],
|
|
451
|
+
["Elementi correlati", documentation.related],
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
sectionMap.forEach(([title, items]) => {
|
|
455
|
+
if (items.length === 0) return;
|
|
456
|
+
sections.push("", `## ${title}`, "", renderList(items));
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (documentation.return) sections.push("", "## Output", "", documentation.return);
|
|
460
|
+
|
|
461
|
+
const runs = [...markup.matchAll(/`+/g)].map(match => match[0].length);
|
|
462
|
+
const fence = "`".repeat(Math.max(3, (runs.length ? Math.max(...runs) : 0) + 1));
|
|
463
|
+
sections.push("", "## Esempio", "", `${fence}html`, markup, fence, "");
|
|
464
|
+
return sections.join("\n");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function printErrors(errors) {
|
|
468
|
+
errors.forEach(error => {
|
|
469
|
+
console.error(`[ERRORE] ${error.file}`);
|
|
470
|
+
console.error(error.message);
|
|
471
|
+
if (error.section) console.error(`Sezione: ${error.section}`);
|
|
472
|
+
if (error.value) console.error(`Valore: ${error.value}`);
|
|
473
|
+
console.error(`Suggerimento: ${error.suggestion}`);
|
|
474
|
+
console.error("");
|
|
475
|
+
});
|
|
476
|
+
console.error(`Generazione annullata: ${errors.length} errori bloccanti.`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function main() {
|
|
480
|
+
const templates = walkFiles(TEMPLATE_ROOT, file => path.extname(file).toLowerCase() === ".html");
|
|
481
|
+
const errors = [];
|
|
482
|
+
const outputPaths = new Map();
|
|
483
|
+
|
|
484
|
+
templates.forEach(template => {
|
|
485
|
+
const output = toPosix(path.relative(DOCS_ROOT, getOutputPath(template))).toLowerCase();
|
|
486
|
+
if (!outputPaths.has(output)) outputPaths.set(output, []);
|
|
487
|
+
outputPaths.get(output).push(template);
|
|
488
|
+
});
|
|
489
|
+
outputPaths.forEach((files, output) => {
|
|
490
|
+
if (files.length < 2) return;
|
|
491
|
+
errors.push(createError(relativeToProject(files[0]), `Collisione del percorso di output "docs/${output}" tra: ${files.map(relativeToProject).join(", ")}.`, "Rinomina uno dei template per ottenere percorsi Markdown distinti."));
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const sources = loadSources();
|
|
495
|
+
const allSourceContent = sources.map(source => source.content).join("\n");
|
|
496
|
+
const parsedTemplates = templates.map(template => {
|
|
497
|
+
const source = fs.readFileSync(template, "utf8");
|
|
498
|
+
const parsed = parseDocumentationBlock(template, source);
|
|
499
|
+
errors.push(...validateTemplate(template, parsed, sources, allSourceContent));
|
|
500
|
+
return { template, parsed };
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
if (errors.length > 0) {
|
|
504
|
+
printErrors(errors);
|
|
505
|
+
process.exitCode = 1;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const expected = new Set();
|
|
510
|
+
const counts = { created: 0, updated: 0, unchanged: 0, removed: 0 };
|
|
511
|
+
|
|
512
|
+
parsedTemplates.forEach(({ template, parsed }) => {
|
|
513
|
+
const output = getOutputPath(template);
|
|
514
|
+
const content = renderMarkdown(template, parsed.data, parsed.markup);
|
|
515
|
+
expected.add(path.resolve(output));
|
|
516
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
517
|
+
|
|
518
|
+
if (!fs.existsSync(output)) {
|
|
519
|
+
fs.writeFileSync(output, content, "utf8");
|
|
520
|
+
counts.created += 1;
|
|
521
|
+
console.log(`[CREATO] ${relativeToProject(output)}`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const existing = normalizeEol(fs.readFileSync(output, "utf8"));
|
|
526
|
+
if (existing === content) {
|
|
527
|
+
counts.unchanged += 1;
|
|
528
|
+
console.log(`[INVARIATO] ${relativeToProject(output)}`);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
fs.writeFileSync(output, content, "utf8");
|
|
533
|
+
counts.updated += 1;
|
|
534
|
+
console.log(`[AGGIORNATO] ${relativeToProject(output)}`);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
MANAGED_DOC_DIRECTORIES
|
|
538
|
+
.flatMap(directory => walkFiles(directory, file => path.extname(file).toLowerCase() === ".md"))
|
|
539
|
+
.forEach(file => {
|
|
540
|
+
if (expected.has(path.resolve(file))) return;
|
|
541
|
+
const firstLine = normalizeEol(fs.readFileSync(file, "utf8")).split("\n", 1)[0];
|
|
542
|
+
if (!GENERATED_MARKER.test(firstLine)) return;
|
|
543
|
+
fs.unlinkSync(file);
|
|
544
|
+
counts.removed += 1;
|
|
545
|
+
console.log(`[RIMOSSO] ${relativeToProject(file)}`);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
console.log("");
|
|
549
|
+
console.log(`Riepilogo: ${templates.length} template validati; ${counts.created} creati; ${counts.updated} aggiornati; ${counts.unchanged} invariati; ${counts.removed} obsoleti rimossi.`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
main();
|
|
554
|
+
} catch (error) {
|
|
555
|
+
console.error(`[ERRORE] ${error.message}`);
|
|
556
|
+
process.exitCode = 1;
|
|
557
|
+
}
|