ether-code 0.1.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -0
  3. package/cli/compiler.js +298 -0
  4. package/cli/ether.js +532 -0
  5. package/cli/watcher.js +106 -0
  6. package/generators/css-generator.js +583 -0
  7. package/generators/graphql-generator.js +868 -0
  8. package/generators/html-generator.js +745 -0
  9. package/generators/js-generator.js +909 -0
  10. package/generators/node-generator.js +467 -0
  11. package/generators/php-generator.js +706 -0
  12. package/generators/python-generator.js +913 -0
  13. package/generators/react-generator.js +599 -0
  14. package/generators/ruby-generator.js +904 -0
  15. package/generators/sql-generator.js +988 -0
  16. package/generators/ts-generator.js +569 -0
  17. package/i18n/i18n-css.json +743 -0
  18. package/i18n/i18n-graphql.json +1531 -0
  19. package/i18n/i18n-html.json +572 -0
  20. package/i18n/i18n-js.json +2790 -0
  21. package/i18n/i18n-node.json +2442 -0
  22. package/i18n/i18n-php.json +4306 -0
  23. package/i18n/i18n-python.json +3080 -0
  24. package/i18n/i18n-react.json +1784 -0
  25. package/i18n/i18n-ruby.json +1858 -0
  26. package/i18n/i18n-sql.json +3466 -0
  27. package/i18n/i18n-ts.json +442 -0
  28. package/lexer/ether-lexer.js +728 -0
  29. package/lexer/tokens.js +292 -0
  30. package/package.json +45 -0
  31. package/parsers/ast-css.js +545 -0
  32. package/parsers/ast-graphql.js +424 -0
  33. package/parsers/ast-html.js +886 -0
  34. package/parsers/ast-js.js +750 -0
  35. package/parsers/ast-node.js +2440 -0
  36. package/parsers/ast-php.js +957 -0
  37. package/parsers/ast-react.js +580 -0
  38. package/parsers/ast-ruby.js +895 -0
  39. package/parsers/ast-ts.js +1352 -0
  40. package/parsers/css-parser.js +1981 -0
  41. package/parsers/graphql-parser.js +2011 -0
  42. package/parsers/html-parser.js +1181 -0
  43. package/parsers/js-parser.js +2564 -0
  44. package/parsers/node-parser.js +2644 -0
  45. package/parsers/php-parser.js +3037 -0
  46. package/parsers/react-parser.js +1035 -0
  47. package/parsers/ruby-parser.js +2680 -0
  48. package/parsers/ts-parser.js +3881 -0
@@ -0,0 +1,745 @@
1
+ const fs = require('fs')
2
+
3
+ class HTMLGenerator {
4
+ constructor(i18nPath = null) {
5
+ this.i18n = null
6
+ this.indent = 0
7
+ this.output = ''
8
+ this.tagMap = {}
9
+ this.attrMap = {}
10
+ this.eventMap = {}
11
+ this.inputTypeMap = {}
12
+ this.voidElements = [
13
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
14
+ 'link', 'meta', 'param', 'source', 'track', 'wbr'
15
+ ]
16
+
17
+ this.buildGenericMaps()
18
+
19
+ if (i18nPath) {
20
+ this.loadI18n(i18nPath)
21
+ }
22
+ }
23
+
24
+ buildGenericMaps() {
25
+ this.tagMap = {
26
+ 'division': 'div', 'paragraphe': 'p', 'portee': 'span',
27
+ 'titre1': 'h1', 'titre2': 'h2', 'titre3': 'h3', 'titre4': 'h4', 'titre5': 'h5', 'titre6': 'h6',
28
+ 'lien': 'a', 'image': 'img', 'bouton': 'button',
29
+ 'formulaire': 'form', 'champ': 'input', 'zone texte': 'textarea',
30
+ 'sélection': 'select', 'selection': 'select', 'option': 'option', 'étiquette': 'label', 'étiquette': 'label',
31
+ 'liste': 'ul', 'liste ordonnée': 'ol', 'élément': 'li', 'élément liste': 'li',
32
+ 'tableau': 'table', 'ligne': 'tr', 'cellule': 'td', 'entête cellule': 'th', 'entete cellule': 'th',
33
+ 'corps tableau': 'tbody', 'entête tableau': 'thead', 'pied tableau': 'tfoot',
34
+ 'entête': 'header', 'piedpage': 'footer', 'navigation': 'nav',
35
+ 'section': 'section', 'article': 'article', 'côté': 'aside', 'principal': 'main',
36
+ 'figure': 'figure', 'legende': 'figcaption',
37
+ 'gras': 'strong', 'italique': 'em', 'souligne': 'u', 'barre': 's',
38
+ 'code': 'code', 'preformate': 'pre', 'citation': 'blockquote',
39
+ 'abreviation': 'abbr', 'temps': 'time', 'marque': 'mark',
40
+ 'indice': 'sub', 'exposant': 'sup',
41
+ 'video': 'video', 'audio': 'audio', 'cadre': 'iframe', 'toile': 'canvas',
42
+ 'details': 'details', 'résumé': 'summary', 'resume': 'summary', 'dialogue': 'dialog',
43
+ 'progression': 'progress', 'metre': 'meter',
44
+ 'groupe champs': 'fieldset', 'legende groupe': 'legend',
45
+ 'liste données': 'datalist', 'liste donnees': 'datalist', 'sortie': 'output',
46
+ 'modele': 'template', 'emplacement': 'slot',
47
+ 'saut ligne': 'br', 'ligne horizontale': 'hr', 'cesure': 'wbr', 'meta': 'meta',
48
+ 'lien externe': 'link'
49
+ }
50
+
51
+ this.attrMap = {
52
+ 'classe': 'class', 'identifiant': 'id', 'nom': 'name', 'valeur': 'value',
53
+ 'type': 'type', 'adresse': 'href', 'source': 'src', 'alternatif': 'alt',
54
+ 'titre': 'title', 'cible': 'target', 'relation': 'rel',
55
+ 'largeur': 'width', 'hauteur': 'height',
56
+ 'placeholder': 'placeholder', 'indicateur': 'placeholder',
57
+ 'requis': 'required', 'desactive': 'disabled', 'lecture seule': 'readonly',
58
+ 'coche': 'checked', 'selectionne': 'selected',
59
+ 'minimum': 'min', 'maximum': 'max', 'pas': 'step',
60
+ 'lignes': 'rows', 'colonnes': 'cols',
61
+ 'fusion colonnes': 'colspan', 'fusion lignes': 'rowspan',
62
+ 'pour': 'for', 'méthode': 'method', 'action': 'action',
63
+ 'accepter': 'accept', 'autocomplete': 'autocomplete',
64
+ 'focus auto': 'autofocus', 'motif': 'pattern',
65
+ 'longueur min': 'minlength', 'longueur max': 'maxlength',
66
+ 'controles': 'controls', 'boucle': 'loop', 'muet': 'muted', 'lecture auto': 'autoplay',
67
+ 'editable': 'contenteditable', 'deplacable': 'draggable', 'cache': 'hidden',
68
+ 'index tabulation': 'tabindex', 'langue': 'lang', 'direction': 'dir',
69
+ 'chargement': 'loading', 'datetime': 'datetime',
70
+ 'aria étiquette': 'aria-label', 'aria decrit par': 'aria-describedby',
71
+ 'aria cache': 'aria-hidden', 'aria étendu': 'aria-expanded',
72
+ 'role': 'role', 'valeur défaut': 'defaultValue', 'coché défaut': 'defaultChecked'
73
+ }
74
+
75
+ this.attrValueMap = {
76
+ 'nouvelle fenetre': '_blank', 'nouvel onglet': '_blank',
77
+ 'meme fenetre': '_self', 'parent': '_parent', 'haut': '_top',
78
+ 'paresseux': 'lazy', 'immediat': 'eager'
79
+ }
80
+
81
+ this.eventMap = {
82
+ 'au clic': 'onclick', 'au double clic': 'ondblclick',
83
+ 'au changement': 'onchange', 'a la soumission': 'onsubmit',
84
+ 'au survol': 'onmouseover', 'au survol fin': 'onmouseout',
85
+ 'au focus': 'onfocus', 'au focus perdu': 'onblur',
86
+ 'touche bas': 'onkeydown', 'touche haut': 'onkeyup',
87
+ 'au chargement': 'onload', 'erreur': 'onerror'
88
+ }
89
+
90
+ this.inputTypeMap = {
91
+ 'texte': 'text', 'mot de passe': 'password', 'courriel': 'email',
92
+ 'nombre': 'number', 'case a cocher': 'checkbox', 'bouton radio': 'radio',
93
+ 'fichier': 'file', 'date': 'date', 'intervalle': 'range',
94
+ 'soumettre': 'submit', 'reinitialiser': 'reset', 'cache': 'hidden'
95
+ }
96
+ }
97
+
98
+ loadI18n(filePath) {
99
+ const content = fs.readFileSync(filePath, 'utf-8')
100
+ this.i18n = JSON.parse(content)
101
+ this.buildMaps()
102
+ }
103
+
104
+ buildMaps() {
105
+ this.tagMap = {}
106
+ this.attrMap = {}
107
+ this.eventMap = {}
108
+
109
+ if (!this.i18n) return
110
+
111
+ const addToMap = (map, section, targetKey = 'html') => {
112
+ if (!section) return
113
+ for (const [key, translations] of Object.entries(section)) {
114
+ if (translations && translations.fr && translations[targetKey]) {
115
+ map[translations.fr.toLowerCase()] = translations[targetKey]
116
+ }
117
+ }
118
+ }
119
+
120
+ addToMap(this.tagMap, this.i18n.rootElements)
121
+ addToMap(this.tagMap, this.i18n.metadata)
122
+ addToMap(this.tagMap, this.i18n.semanticSections)
123
+ addToMap(this.tagMap, this.i18n.headings)
124
+ addToMap(this.tagMap, this.i18n.textContent)
125
+ addToMap(this.tagMap, this.i18n.inlineSemantics)
126
+ addToMap(this.tagMap, this.i18n.lists)
127
+ addToMap(this.tagMap, this.i18n.textFormatting)
128
+ addToMap(this.tagMap, this.i18n.codeAndData)
129
+ addToMap(this.tagMap, this.i18n.images)
130
+ addToMap(this.tagMap, this.i18n.media)
131
+ addToMap(this.tagMap, this.i18n.embeddedContent)
132
+ addToMap(this.tagMap, this.i18n.graphics)
133
+ addToMap(this.tagMap, this.i18n.tableStructure)
134
+ addToMap(this.tagMap, this.i18n.formStructure)
135
+ addToMap(this.tagMap, this.i18n.formInputs)
136
+ addToMap(this.tagMap, this.i18n.interactive)
137
+ addToMap(this.tagMap, this.i18n.genericContainers)
138
+
139
+ addToMap(this.attrMap, this.i18n.globalAttributes)
140
+ addToMap(this.attrMap, this.i18n.linkAttributes)
141
+ addToMap(this.attrMap, this.i18n.imageAttributes)
142
+ addToMap(this.attrMap, this.i18n.formAttributes)
143
+ addToMap(this.attrMap, this.i18n.inputAttributes)
144
+ addToMap(this.attrMap, this.i18n.textareaAttributes)
145
+ addToMap(this.attrMap, this.i18n.mediaAttributes)
146
+ addToMap(this.attrMap, this.i18n.tableAttributes)
147
+ addToMap(this.attrMap, this.i18n.metaAttributes)
148
+ addToMap(this.attrMap, this.i18n.ariaStates)
149
+ addToMap(this.attrMap, this.i18n.ariaProperties)
150
+
151
+ addToMap(this.eventMap, this.i18n.windowEvents)
152
+ addToMap(this.eventMap, this.i18n.mouseEvents)
153
+ addToMap(this.eventMap, this.i18n.keyboardEvents)
154
+ addToMap(this.eventMap, this.i18n.formEvents)
155
+ addToMap(this.eventMap, this.i18n.mediaEvents)
156
+ addToMap(this.eventMap, this.i18n.dragEvents)
157
+ addToMap(this.eventMap, this.i18n.clipboardEvents)
158
+ addToMap(this.eventMap, this.i18n.touchEvents)
159
+ addToMap(this.eventMap, this.i18n.pointerEvents)
160
+ }
161
+
162
+ translateTag(tag) {
163
+ const lower = tag.toLowerCase()
164
+ return this.tagMap[lower] || this.translateGenericTag(tag)
165
+ }
166
+
167
+ translateAttribute(attr) {
168
+ const lower = attr.toLowerCase()
169
+ return this.attrMap[lower] || this.eventMap[lower] || this.translateGenericAttr(attr)
170
+ }
171
+
172
+ translateGenericTag(tag) {
173
+ const translations = {
174
+ 'document': 'html',
175
+ 'tete': 'head',
176
+ 'corps': 'body',
177
+ 'titre': 'title',
178
+ 'meta': 'meta',
179
+ 'lien': 'link',
180
+ 'style': 'style',
181
+ 'script': 'script',
182
+ 'entête': 'header',
183
+ 'piedpage': 'footer',
184
+ 'navigation': 'nav',
185
+ 'principal': 'main',
186
+ 'section': 'section',
187
+ 'article': 'article',
188
+ 'aside': 'aside',
189
+ 'côté': 'aside',
190
+ 'recherche': 'search',
191
+ 'adresse': 'address',
192
+ 'titre1': 'h1',
193
+ 'titre2': 'h2',
194
+ 'titre3': 'h3',
195
+ 'titre4': 'h4',
196
+ 'titre5': 'h5',
197
+ 'titre6': 'h6',
198
+ 'paragraphe': 'p',
199
+ 'division': 'div',
200
+ 'span': 'span',
201
+ 'portee': 'span',
202
+ 'lien': 'a',
203
+ 'image': 'img',
204
+ 'figure': 'figure',
205
+ 'legende': 'figcaption',
206
+ 'liste': 'ul',
207
+ 'liste ordonnée': 'ol',
208
+ 'élément': 'li',
209
+ 'definition': 'dl',
210
+ 'terme': 'dt',
211
+ 'description': 'dd',
212
+ 'tableau': 'table',
213
+ 'ligne': 'tr',
214
+ 'cellule': 'td',
215
+ 'entête cellule': 'th',
216
+ 'entete cellule': 'th',
217
+ 'entête tableau': 'thead',
218
+ 'corps tableau': 'tbody',
219
+ 'pied tableau': 'tfoot',
220
+ 'formulaire': 'form',
221
+ 'étiquette': 'label',
222
+ 'étiquette': 'label',
223
+ 'champ': 'input',
224
+ 'zone texte': 'textarea',
225
+ 'sélection': 'select',
226
+ 'option': 'option',
227
+ 'groupe options': 'optgroup',
228
+ 'bouton': 'button',
229
+ 'groupe champs': 'fieldset',
230
+ 'legende groupe': 'legend',
231
+ 'video': 'video',
232
+ 'audio': 'audio',
233
+ 'source': 'source',
234
+ 'piste': 'track',
235
+ 'cadre': 'iframe',
236
+ 'objet': 'object',
237
+ 'integrer': 'embed',
238
+ 'canvas': 'canvas',
239
+ 'toile': 'canvas',
240
+ 'svg': 'svg',
241
+ 'math': 'math',
242
+ 'gras': 'strong',
243
+ 'fort': 'strong',
244
+ 'italique': 'em',
245
+ 'emphase': 'em',
246
+ 'souligne': 'u',
247
+ 'barre': 's',
248
+ 'supprime': 'del',
249
+ 'insere': 'ins',
250
+ 'petit': 'small',
251
+ 'marque': 'mark',
252
+ 'code': 'code',
253
+ 'preformate': 'pre',
254
+ 'citation': 'blockquote',
255
+ 'citation courte': 'q',
256
+ 'abreviation': 'abbr',
257
+ 'temps': 'time',
258
+ 'variable': 'var',
259
+ 'clavier': 'kbd',
260
+ 'exemple': 'samp',
261
+ 'indice': 'sub',
262
+ 'exposant': 'sup',
263
+ 'ligne horizontale': 'hr',
264
+ 'saut ligne': 'br',
265
+ 'retour ligne': 'br',
266
+ 'details': 'details',
267
+ 'résumé': 'summary',
268
+ 'resume': 'summary',
269
+ 'dialogue': 'dialog',
270
+ 'menu': 'menu',
271
+ 'modele': 'template',
272
+ 'slot': 'slot',
273
+ 'emplacement': 'slot',
274
+ 'données': 'data',
275
+ 'metre': 'meter',
276
+ 'progression': 'progress',
277
+ 'sortie': 'output',
278
+ 'carte': 'map',
279
+ 'zone cliquable': 'area',
280
+ 'groupe colonnes': 'colgroup',
281
+ 'colonne': 'col',
282
+ 'legende tableau': 'caption',
283
+ 'wbr': 'wbr',
284
+ 'cesure': 'wbr',
285
+ 'noscript': 'noscript',
286
+ 'sans script': 'noscript',
287
+ 'liste données': 'datalist',
288
+ 'liste donnees': 'datalist'
289
+ }
290
+
291
+ const lower = tag.toLowerCase()
292
+ return translations[lower] || tag
293
+ }
294
+
295
+ translateGenericAttr(attr) {
296
+ const translations = {
297
+ 'identifiant': 'id',
298
+ 'classe': 'class',
299
+ 'style': 'style',
300
+ 'titre': 'title',
301
+ 'langue': 'lang',
302
+ 'direction': 'dir',
303
+ 'cache': 'hidden',
304
+ 'tabindex': 'tabindex',
305
+ 'index tabulation': 'tabindex',
306
+ 'editable': 'contenteditable',
307
+ 'draggable': 'draggable',
308
+ 'deplacable': 'draggable',
309
+ 'role': 'role',
310
+ 'adresse': 'href',
311
+ 'cible': 'target',
312
+ 'nouvelle fenetre': '_blank',
313
+ 'source': 'src',
314
+ 'alt': 'alt',
315
+ 'alternatif': 'alt',
316
+ 'largeur': 'width',
317
+ 'hauteur': 'height',
318
+ 'chargement': 'loading',
319
+ 'paresseux': 'lazy',
320
+ 'action': 'action',
321
+ 'méthode': 'method',
322
+ 'type': 'type',
323
+ 'nom': 'name',
324
+ 'valeur': 'value',
325
+ 'placeholder': 'placeholder',
326
+ 'indicateur': 'placeholder',
327
+ 'requis': 'required',
328
+ 'obligatoire': 'required',
329
+ 'desactive': 'disabled',
330
+ 'lecture seule': 'readonly',
331
+ 'coche': 'checked',
332
+ 'selectionne': 'selected',
333
+ 'multiple': 'multiple',
334
+ 'minimum': 'min',
335
+ 'maximum': 'max',
336
+ 'pas': 'step',
337
+ 'motif': 'pattern',
338
+ 'longueur min': 'minlength',
339
+ 'longueur max': 'maxlength',
340
+ 'autocomplete': 'autocomplete',
341
+ 'autofocus': 'autofocus',
342
+ 'focus auto': 'autofocus',
343
+ 'lignes': 'rows',
344
+ 'colonnes': 'cols',
345
+ 'controles': 'controls',
346
+ 'lecture auto': 'autoplay',
347
+ 'boucle': 'loop',
348
+ 'muet': 'muted',
349
+ 'affiche': 'poster',
350
+ 'prechargement': 'preload',
351
+ 'fusion lignes': 'rowspan',
352
+ 'fusion colonnes': 'colspan',
353
+ 'portee': 'scope',
354
+ 'pour': 'for',
355
+ 'accept': 'accept',
356
+ 'accepter': 'accept',
357
+ 'enctype': 'enctype',
358
+ 'encodage': 'enctype',
359
+ 'rel': 'rel',
360
+ 'relation': 'rel',
361
+ 'media': 'media',
362
+ 'type media': 'media',
363
+ 'charset': 'charset',
364
+ 'encodage caractères': 'charset',
365
+ 'asynchrone': 'asynchrone',
366
+ 'asynchrone': 'asynchrone',
367
+ 'defer': 'defer',
368
+ 'differer': 'defer',
369
+ 'crossorigin': 'crossorigin',
370
+ 'origine croisee': 'crossorigin',
371
+ 'integrite': 'integrity',
372
+ 'referrerpolicy': 'referrerpolicy',
373
+ 'politique referent': 'referrerpolicy',
374
+ 'sandbox': 'sandbox',
375
+ 'bac a sable': 'sandbox',
376
+ 'allow': 'allow',
377
+ 'autoriser': 'allow',
378
+ 'données': 'data',
379
+ 'accesskey': 'accesskey',
380
+ 'touche acces': 'accesskey',
381
+ 'translate': 'translate',
382
+ 'traduire': 'translate',
383
+ 'spellcheck': 'spellcheck',
384
+ 'orthographe': 'spellcheck',
385
+ 'enterkeyhint': 'enterkeyhint',
386
+ 'indice entrée': 'enterkeyhint',
387
+ 'inputmode': 'inputmode',
388
+ 'mode saisie': 'inputmode',
389
+ 'is': 'is',
390
+ 'est': 'is',
391
+ 'part': 'part',
392
+ 'partie': 'part',
393
+ 'exportparts': 'exportparts',
394
+ 'exporter parties': 'exportparts',
395
+ 'nonce': 'nonce',
396
+ 'jeton': 'nonce'
397
+ }
398
+
399
+ const lower = attr.toLowerCase()
400
+ return translations[lower] || attr
401
+ }
402
+
403
+ translateInputType(type) {
404
+ const types = {
405
+ 'texte': 'text',
406
+ 'mot de passe': 'password',
407
+ 'email': 'email',
408
+ 'courriel': 'email',
409
+ 'nombre': 'number',
410
+ 'telephone': 'tel',
411
+ 'url': 'url',
412
+ 'recherche': 'search',
413
+ 'date': 'date',
414
+ 'heure': 'time',
415
+ 'datetime': 'datetime-local',
416
+ 'date heure': 'datetime-local',
417
+ 'mois': 'month',
418
+ 'semaine': 'week',
419
+ 'couleur': 'color',
420
+ 'fichier': 'file',
421
+ 'cache': 'hidden',
422
+ 'case a cocher': 'checkbox',
423
+ 'bouton radio': 'radio',
424
+ 'intervalle': 'range',
425
+ 'soumettre': 'submit',
426
+ 'reinitialiser': 'reset',
427
+ 'bouton': 'button',
428
+ 'image': 'image'
429
+ }
430
+
431
+ const lower = type.toLowerCase()
432
+ return types[lower] || type
433
+ }
434
+
435
+ generate(ast) {
436
+ this.output = ''
437
+ this.indent = 0
438
+
439
+ if (Array.isArray(ast)) {
440
+ for (const node of ast) {
441
+ this.generateNode(node)
442
+ }
443
+ } else if (ast && ast.type) {
444
+ this.generateNode(ast)
445
+ } else if (ast && ast.children) {
446
+ for (const child of ast.children) {
447
+ this.generateNode(child)
448
+ }
449
+ }
450
+
451
+ return this.output.trim()
452
+ }
453
+
454
+ generateNode(node) {
455
+ if (!node) return
456
+
457
+ switch (node.type) {
458
+ case 'Element':
459
+ case 'élément':
460
+ this.generateElement(node)
461
+ break
462
+ case 'Text':
463
+ case 'text':
464
+ this.generateText(node)
465
+ break
466
+ case 'Comment':
467
+ case 'comment':
468
+ this.generateComment(node)
469
+ break
470
+ case 'Doctype':
471
+ case 'doctype':
472
+ this.generateDoctype(node)
473
+ break
474
+ default:
475
+ if (node.tag || node.tagName) {
476
+ this.generateElement(node)
477
+ } else if (node.text || node.content) {
478
+ this.generateText(node)
479
+ }
480
+ }
481
+ }
482
+
483
+ generateElement(node) {
484
+ const tag = this.translateTag(node.tag || node.tagName || node.name)
485
+ const attributes = this.generateAttributes(node.attributes || node.attrs || {})
486
+ const isVoid = this.voidElements.includes(tag.toLowerCase())
487
+
488
+ if (isVoid) {
489
+ this.writeLine(`<${tag}${attributes}>`)
490
+ return
491
+ }
492
+
493
+ const children = node.children || node.content || []
494
+ const hasChildren = Array.isArray(children) && children.length > 0
495
+ const textContent = typeof children === 'string' ? children :
496
+ (node.text || node.textContent || null)
497
+
498
+ if (textContent && !hasChildren) {
499
+ this.writeLine(`<${tag}${attributes}>${this.escapeHtml(textContent)}</${tag}>`)
500
+ return
501
+ }
502
+
503
+ if (!hasChildren && !textContent) {
504
+ this.writeLine(`<${tag}${attributes}></${tag}>`)
505
+ return
506
+ }
507
+
508
+ this.writeLine(`<${tag}${attributes}>`)
509
+ this.indent++
510
+
511
+ for (const child of children) {
512
+ if (typeof child === 'string') {
513
+ this.writeLine(this.escapeHtml(child))
514
+ } else {
515
+ this.generateNode(child)
516
+ }
517
+ }
518
+
519
+ this.indent--
520
+ this.writeLine(`</${tag}>`)
521
+ }
522
+
523
+ generateAttributes(attrs) {
524
+ if (!attrs || Object.keys(attrs).length === 0) {
525
+ return ''
526
+ }
527
+
528
+ const parts = []
529
+ for (const [key, value] of Object.entries(attrs)) {
530
+ let attrName = this.translateAttribute(key)
531
+
532
+ if (attrName.startsWith('au ') || attrName.startsWith('a la ')) {
533
+ attrName = this.translateEvent(key)
534
+ }
535
+
536
+ if (value === true || value === '') {
537
+ parts.push(attrName)
538
+ } else if (value === false || value === null || value === undefined) {
539
+ continue
540
+ } else {
541
+ let attrValue = this.translateAttrValue(value)
542
+
543
+ if (attrName === 'type' && this.isInputType(value)) {
544
+ attrValue = this.translateInputType(value)
545
+ }
546
+
547
+ parts.push(`${attrName}="${this.escapeAttr(attrValue)}"`)
548
+ }
549
+ }
550
+
551
+ return parts.length > 0 ? ' ' + parts.join(' ') : ''
552
+ }
553
+
554
+ translateAttrValue(value) {
555
+ if (typeof value !== 'string') return value
556
+ const lower = value.toLowerCase()
557
+
558
+ const valueMap = {
559
+ 'nouvelle fenetre': '_blank', 'nouvel onglet': '_blank',
560
+ 'meme fenetre': '_self', 'parent': '_parent', 'haut': '_top',
561
+ 'paresseux': 'lazy', 'immediat': 'eager',
562
+ 'post': 'post', 'get': 'get'
563
+ }
564
+
565
+ return valueMap[lower] || value
566
+ }
567
+
568
+ translateEvent(event) {
569
+ const events = {
570
+ 'au clic': 'onclick',
571
+ 'au double clic': 'ondblclick',
572
+ 'au survol': 'onmouseover',
573
+ 'au survol fin': 'onmouseout',
574
+ 'souris entrée': 'onmouseenter',
575
+ 'souris sortie': 'onmouseleave',
576
+ 'souris bas': 'onmousedown',
577
+ 'souris haut': 'onmouseup',
578
+ 'souris mouvement': 'onmousemove',
579
+ 'au focus': 'onfocus',
580
+ 'au focus perdu': 'onblur',
581
+ 'perte focus': 'onblur',
582
+ 'au changement': 'onchange',
583
+ 'a la saisie': 'oninput',
584
+ 'touche bas': 'onkeydown',
585
+ 'touche haut': 'onkeyup',
586
+ 'touche presse': 'onkeypress',
587
+ 'a la soumission': 'onsubmit',
588
+ 'a la reinitialisation': 'onreset',
589
+ 'au chargement': 'onload',
590
+ 'a la fermeture': 'onunload',
591
+ 'au redimensionnement': 'onresize',
592
+ 'au défilement': 'onscroll',
593
+ 'erreur': 'onerror',
594
+ 'lecture': 'onplay',
595
+ 'pause': 'onpause',
596
+ 'fin': 'onended',
597
+ 'glisser début': 'ondragstart',
598
+ 'glisser': 'ondrag',
599
+ 'glisser fin': 'ondragend',
600
+ 'glisser entrée': 'ondragenter',
601
+ 'glisser sortie': 'ondragleave',
602
+ 'glisser survol': 'ondragover',
603
+ 'deposer': 'ondrop',
604
+ 'copier': 'oncopy',
605
+ 'couper': 'oncut',
606
+ 'coller': 'onpaste',
607
+ 'toucher début': 'ontouchstart',
608
+ 'toucher mouvement': 'ontouchmove',
609
+ 'toucher fin': 'ontouchend',
610
+ 'toucher annuler': 'ontouchcancel',
611
+ 'pointeur bas': 'onpointerdown',
612
+ 'pointeur haut': 'onpointerup',
613
+ 'pointeur mouvement': 'onpointermove',
614
+ 'pointeur entrée': 'onpointerenter',
615
+ 'pointeur sortie': 'onpointerleave',
616
+ 'roue': 'onwheel',
617
+ 'menu contextuel': 'oncontextmenu',
618
+ 'animation début': 'onanimationstart',
619
+ 'animation fin': 'onanimationend',
620
+ 'transition fin': 'ontransitionend'
621
+ }
622
+
623
+ const lower = event.toLowerCase()
624
+ return events[lower] || event
625
+ }
626
+
627
+ isInputType(value) {
628
+ const types = [
629
+ 'texte', 'text', 'mot de passe', 'password', 'email', 'courriel',
630
+ 'nombre', 'number', 'telephone', 'tel', 'url', 'recherche', 'search',
631
+ 'date', 'heure', 'time', 'datetime', 'date heure', 'mois', 'month',
632
+ 'semaine', 'week', 'couleur', 'color', 'fichier', 'file', 'cache',
633
+ 'hidden', 'case a cocher', 'checkbox', 'bouton radio', 'radio',
634
+ 'intervalle', 'range', 'soumettre', 'submit', 'reinitialiser', 'reset',
635
+ 'bouton', 'button', 'image'
636
+ ]
637
+ return types.includes(value.toLowerCase())
638
+ }
639
+
640
+ generateText(node) {
641
+ const text = node.text || node.content || node.value || ''
642
+ if (text.trim()) {
643
+ this.writeLine(this.escapeHtml(text))
644
+ }
645
+ }
646
+
647
+ generateComment(node) {
648
+ const comment = node.comment || node.content || node.text || ''
649
+ this.writeLine(`<!-- ${comment} -->`)
650
+ }
651
+
652
+ generateDoctype(node) {
653
+ this.output += '<!DOCTYPE html>\n'
654
+ }
655
+
656
+ writeLine(text) {
657
+ const indentation = ' '.repeat(this.indent)
658
+ this.output += indentation + text + '\n'
659
+ }
660
+
661
+ escapeHtml(text) {
662
+ if (typeof text !== 'string') return text
663
+ return text
664
+ .replace(/&/g, '&amp;')
665
+ .replace(/</g, '&lt;')
666
+ .replace(/>/g, '&gt;')
667
+ }
668
+
669
+ escapeAttr(text) {
670
+ if (typeof text !== 'string') return text
671
+ return text
672
+ .replace(/&/g, '&amp;')
673
+ .replace(/"/g, '&quot;')
674
+ .replace(/'/g, '&#39;')
675
+ .replace(/</g, '&lt;')
676
+ .replace(/>/g, '&gt;')
677
+ }
678
+
679
+ generateFromSimple(structure) {
680
+ this.output = ''
681
+ this.indent = 0
682
+
683
+ this.generateSimpleNode(structure)
684
+
685
+ return this.output.trim()
686
+ }
687
+
688
+ generateSimpleNode(node) {
689
+ if (typeof node === 'string') {
690
+ this.writeLine(this.escapeHtml(node))
691
+ return
692
+ }
693
+
694
+ for (const [tag, content] of Object.entries(node)) {
695
+ const translatedTag = this.translateTag(tag)
696
+ const isVoid = this.voidElements.includes(translatedTag.toLowerCase())
697
+
698
+ if (typeof content === 'string') {
699
+ if (isVoid) {
700
+ this.writeLine(`<${translatedTag}>`)
701
+ } else {
702
+ this.writeLine(`<${translatedTag}>${this.escapeHtml(content)}</${translatedTag}>`)
703
+ }
704
+ } else if (typeof content === 'object' && !Array.isArray(content)) {
705
+ const attrs = content._attrs || content._attributs || {}
706
+ const children = content._enfants || content._children || content._contenu || content._content
707
+ const text = content._texte || content._text
708
+
709
+ const attrStr = this.generateAttributes(attrs)
710
+
711
+ if (isVoid) {
712
+ this.writeLine(`<${translatedTag}${attrStr}>`)
713
+ } else if (text) {
714
+ this.writeLine(`<${translatedTag}${attrStr}>${this.escapeHtml(text)}</${translatedTag}>`)
715
+ } else if (children) {
716
+ this.writeLine(`<${translatedTag}${attrStr}>`)
717
+ this.indent++
718
+ if (Array.isArray(children)) {
719
+ for (const child of children) {
720
+ this.generateSimpleNode(child)
721
+ }
722
+ } else {
723
+ this.generateSimpleNode(children)
724
+ }
725
+ this.indent--
726
+ this.writeLine(`</${translatedTag}>`)
727
+ } else {
728
+ this.writeLine(`<${translatedTag}${attrStr}></${translatedTag}>`)
729
+ }
730
+ } else if (Array.isArray(content)) {
731
+ this.writeLine(`<${translatedTag}>`)
732
+ this.indent++
733
+ for (const child of content) {
734
+ this.generateSimpleNode(child)
735
+ }
736
+ this.indent--
737
+ this.writeLine(`</${translatedTag}>`)
738
+ }
739
+ }
740
+ }
741
+ }
742
+
743
+ module.exports = {
744
+ HTMLGenerator
745
+ }