ether-code 0.1.5 → 0.1.7

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 (44) hide show
  1. package/cli/ether.js +1 -1
  2. package/generators/css-generator.js +42 -55
  3. package/generators/graphql-generator.js +19 -22
  4. package/generators/html-generator.js +51 -182
  5. package/generators/js-generator.js +76 -157
  6. package/generators/node-generator.js +49 -93
  7. package/generators/php-generator.js +46 -68
  8. package/generators/python-generator.js +35 -54
  9. package/generators/react-generator.js +37 -47
  10. package/generators/ruby-generator.js +59 -119
  11. package/generators/sql-generator.js +42 -63
  12. package/generators/ts-generator.js +59 -133
  13. package/i18n/i18n-css.json +147 -147
  14. package/i18n/i18n-graphql.json +6 -6
  15. package/i18n/i18n-html.json +135 -135
  16. package/i18n/i18n-js.json +107 -107
  17. package/i18n/i18n-node.json +14 -14
  18. package/i18n/i18n-php.json +177 -177
  19. package/i18n/i18n-python.json +16 -16
  20. package/i18n/i18n-react.json +97 -97
  21. package/i18n/i18n-ruby.json +22 -22
  22. package/i18n/i18n-sql.json +153 -153
  23. package/i18n/i18n-ts.json +10 -10
  24. package/lexer/ether-lexer.js +175 -34
  25. package/lexer/tokens.js +6 -6
  26. package/package.json +1 -1
  27. package/parsers/ast-css.js +0 -545
  28. package/parsers/ast-graphql.js +0 -424
  29. package/parsers/ast-html.js +0 -886
  30. package/parsers/ast-js.js +0 -750
  31. package/parsers/ast-node.js +0 -2440
  32. package/parsers/ast-php.js +0 -957
  33. package/parsers/ast-react.js +0 -580
  34. package/parsers/ast-ruby.js +0 -895
  35. package/parsers/ast-ts.js +0 -1352
  36. package/parsers/css-parser.js +0 -1981
  37. package/parsers/graphql-parser.js +0 -2011
  38. package/parsers/html-parser.js +0 -1182
  39. package/parsers/js-parser.js +0 -2564
  40. package/parsers/node-parser.js +0 -2644
  41. package/parsers/php-parser.js +0 -3037
  42. package/parsers/react-parser.js +0 -1035
  43. package/parsers/ruby-parser.js +0 -2680
  44. package/parsers/ts-parser.js +0 -3881
@@ -1,1182 +0,0 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
- const AST = require('./ast-html')
4
-
5
- const VOID_ELEMENTS = new Set([
6
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
7
- 'link', 'meta', 'source', 'track', 'wbr'
8
- ])
9
-
10
- class HTMLLexer {
11
- constructor(source, i18n) {
12
- this.source = source
13
- this.i18n = i18n
14
- this.pos = 0
15
- this.line = 1
16
- this.column = 1
17
- this.tokens = []
18
- }
19
-
20
- peek(offset = 0) {
21
- return this.source[this.pos + offset] || ''
22
- }
23
-
24
- advance() {
25
- const char = this.source[this.pos]
26
- this.pos++
27
- if (char === '\n') {
28
- this.line++
29
- this.column = 1
30
- } else {
31
- this.column++
32
- }
33
- return char
34
- }
35
-
36
- skipWhitespaceInline() {
37
- while (this.pos < this.source.length) {
38
- const char = this.peek()
39
- if (char === ' ' || char === '\t' || char === '\r') {
40
- this.advance()
41
- } else {
42
- break
43
- }
44
- }
45
- }
46
-
47
- skipLineComment() {
48
- while (this.pos < this.source.length && this.peek() !== '\n') {
49
- this.advance()
50
- }
51
- }
52
-
53
- skipBlockComment() {
54
- this.advance()
55
- this.advance()
56
- while (this.pos < this.source.length) {
57
- if (this.peek() === '-' && this.peek(1) === '}') {
58
- this.advance()
59
- this.advance()
60
- break
61
- }
62
- this.advance()
63
- }
64
- }
65
-
66
- readIndent() {
67
- let indent = 0
68
- while (this.peek() === ' ') {
69
- indent++
70
- this.advance()
71
- }
72
- while (this.peek() === '\t') {
73
- indent += 4
74
- this.advance()
75
- }
76
- return indent
77
- }
78
-
79
- readString(quote) {
80
- let value = ''
81
- this.advance()
82
- while (this.pos < this.source.length) {
83
- const char = this.peek()
84
- if (char === quote) {
85
- this.advance()
86
- break
87
- }
88
- if (char === '\\') {
89
- this.advance()
90
- const escaped = this.advance()
91
- switch (escaped) {
92
- case 'n': value += '\n'; break
93
- case 't': value += '\t'; break
94
- case 'r': value += '\r'; break
95
- default: value += escaped
96
- }
97
- } else {
98
- value += this.advance()
99
- }
100
- }
101
- return value
102
- }
103
-
104
- readNumber() {
105
- let value = ''
106
- if (this.peek() === '-' || this.peek() === '+') {
107
- value += this.advance()
108
- }
109
- while (/[0-9]/.test(this.peek())) {
110
- value += this.advance()
111
- }
112
- if (this.peek() === '.') {
113
- value += this.advance()
114
- while (/[0-9]/.test(this.peek())) {
115
- value += this.advance()
116
- }
117
- }
118
- return parseFloat(value)
119
- }
120
-
121
- readWord() {
122
- let words = []
123
- let currentWord = ''
124
-
125
- while (this.pos < this.source.length) {
126
- const char = this.peek()
127
- if (/[a-zA-ZÀ-ÿА-яぁ-ゟァ-ヿ一-龯0-9_-]/.test(char)) {
128
- currentWord += this.advance()
129
- } else if (char === ' ' && currentWord) {
130
- const nextChar = this.source[this.pos + 1]
131
- if (nextChar && /[a-zA-ZÀ-ÿА-яぁ-ゟァ-ヿ一-龯0-9]/.test(nextChar)) {
132
- const testWord = currentWord + ' ' + this.peekWord(this.pos + 1)
133
- if (this.isMultiWordKeyword(testWord)) {
134
- words.push(currentWord)
135
- currentWord = ''
136
- this.advance()
137
- } else {
138
- break
139
- }
140
- } else {
141
- break
142
- }
143
- } else {
144
- break
145
- }
146
- }
147
-
148
- if (currentWord) {
149
- words.push(currentWord)
150
- }
151
-
152
- return words.join(' ')
153
- }
154
-
155
- peekWord(startPos) {
156
- let word = ''
157
- let pos = startPos
158
- while (pos < this.source.length) {
159
- const char = this.source[pos]
160
- if (/[a-zA-ZÀ-ÿА-яぁ-ゟァ-ヿ一-龯0-9_-]/.test(char)) {
161
- word += char
162
- pos++
163
- } else {
164
- break
165
- }
166
- }
167
- return word
168
- }
169
-
170
- isMultiWordKeyword(text) {
171
- const multiWordPatterns = [
172
- 'titre 1', 'titre 2', 'titre 3', 'titre 4', 'titre 5', 'titre 6',
173
- 'heading 1', 'heading 2', 'heading 3', 'heading 4', 'heading 5', 'heading 6',
174
- 'título 1', 'título 2', 'título 3', 'título 4', 'título 5', 'título 6',
175
- 'заголовок 1', 'заголовок 2', 'заголовок 3', 'заголовок 4', 'заголовок 5', 'заголовок 6',
176
- '标题 1', '标题 2', '标题 3', '标题 4', '标题 5', '标题 6',
177
- '見出し 1', '見出し 2', '見出し 3', '見出し 4', '見出し 5', '見出し 6',
178
- 'liste ordonnee', 'liste ordonnée', 'liste non ordonnee', 'liste non ordonnée', 'liste description',
179
- 'ordered list', 'unordered list', 'description list',
180
- 'lista ordenada', 'lista no ordenada', 'lista descripción',
181
- 'упорядоченный список', 'неупорядоченный список', 'список описания',
182
- '有序列表', '无序列表', '描述列表',
183
- '順序付きリスト', '順序なしリスト', '説明リスト',
184
- 'retour ligne', 'line break', 'salto linea', 'salto línea', 'перенос строки', '换行', '改行',
185
- 'ligne horizontale', 'horizontal rule', 'linea horizontal', 'línea horizontal', 'горизонтальная линия', '水平线', '水平線',
186
- 'citation bloc', 'blockquote', 'cita bloque', 'блок цитаты', '块引用', 'ブロック引用',
187
- 'legende figure', 'légende figure', 'figure caption', 'leyenda figura', 'подпись рисунка', '图片说明', '図のキャプション',
188
- 'element liste', 'élément liste', 'list item', 'elemento lista', 'элемент списка', '列表项', 'リスト項目',
189
- 'entree clavier', 'entrée clavier', 'keyboard input', 'entrada teclado', 'ввод клавиатуры', '键盘输入', 'キーボード入力',
190
- 'sortie exemple', 'sample output', 'salida ejemplo', 'пример вывода', '示例输出', 'サンプル出力',
191
- 'image reactive', 'image réactive', 'picture', 'imagen reactiva', 'адаптивное изображение', '响应式图片', 'レスポンシブ画像',
192
- 'carte image', 'image map', 'mapa imagen', 'карта изображения', '图像映射', 'イメージマップ',
193
- 'cadre en ligne', 'inline frame', 'marco en linea', 'встроенный фрейм', '内联框架', 'インラインフレーム',
194
- 'entete tableau', 'entête tableau', 'table header', 'encabezado tabla', 'заголовок таблицы', '表头', 'テーブルヘッダー',
195
- 'corps tableau', 'table body', 'cuerpo tabla', 'тело таблицы', '表体', 'テーブル本体',
196
- 'pied tableau', 'table footer', 'pie tabla', 'подвал таблицы', '表脚', 'テーブルフッター',
197
- 'cellule entete', 'cellule entête', 'header cell', 'celda encabezado', 'ячейка заголовка', '表头单元格', 'ヘッダーセル',
198
- 'groupe colonnes', 'column group', 'grupo columnas', 'группа колонок', '列组', '列グループ',
199
- 'champ texte', 'text field', 'campo texto', 'текстовое поле', '文本字段', 'テキストフィールド',
200
- 'zone texte', 'text area', 'area texto', 'área texto', 'текстовая область', '文本区域', 'テキストエリア',
201
- 'case a cocher', 'case à cocher', 'checkbox', 'casilla verificacion', 'casilla verificación', 'флажок', '复选框', 'チェックボックス',
202
- 'bouton radio', 'radio button', 'boton radio', 'botón radio', 'радиокнопка', '单选按钮', 'ラジオボタン',
203
- 'liste deroulante', 'liste déroulante', 'dropdown', 'lista desplegable', 'выпадающий список', '下拉列表', 'ドロップダウン',
204
- 'groupe options', 'option group', 'grupo opciones', 'группа опций', '选项组', 'オプショングループ',
205
- 'groupe champs', 'fieldset', 'grupo campos', 'группа полей', '字段组', 'フィールドセット',
206
- 'nouvelle fenetre', 'nouvelle fenêtre', 'new window', 'nueva ventana', 'новое окно', '新窗口', '新しいウィンドウ',
207
- 'meme fenetre', 'même fenêtre', 'same window', 'misma ventana', 'то же окно', '同一窗口', '同じウィンドウ',
208
- 'lien vers', 'link to', 'enlace a', 'ссылка на', '链接到', 'リンク先',
209
- 'isolation bidi', 'bidi isolation', 'aislamiento bidi', 'изоляция bidi', '双向隔离', '双方向分離',
210
- 'remplacement bidi', 'bidi override', 'reemplazo bidi', 'переопределение bidi', '双向覆盖', '双方向オーバーライド',
211
- 'annotation ruby', 'ruby text', 'anotacion ruby', 'anotación ruby', 'аннотация ruby', 'ruby注释', 'ルビ注釈',
212
- 'parentheses ruby', 'parenthèses ruby', 'ruby parenthesis', 'paréntesis ruby', 'скобки ruby', 'ruby括号', 'ルビ括弧',
213
- 'retour ligne possible', 'word break opportunity', 'oportunidad salto', 'возможность переноса', '换行机会', '改行可能',
214
- 'lecture auto', 'autoplay', 'reproduccion automatica', 'reproducción automática', 'автовоспроизведение', '自动播放', '自動再生',
215
- 'en boucle', 'loop', 'en bucle', 'цикл', '循环', 'ループ',
216
- 'lecture seule', 'readonly', 'solo lectura', 'только чтение', '只读', '読み取り専用',
217
- 'champ email', 'email field', 'campo email', 'поле email', '邮箱字段', 'メールフィールド',
218
- 'champ mot de passe', 'password field', 'campo contraseña', 'поле пароля', '密码字段', 'パスワードフィールド',
219
- 'champ numero', 'champ numéro', 'number field', 'campo numero', 'campo número', 'числовое поле', '数字字段', '数値フィールド',
220
- 'champ date', 'date field', 'campo fecha', 'поле даты', '日期字段', '日付フィールド',
221
- 'champ heure', 'time field', 'campo hora', 'поле времени', '时间字段', '時間フィールド',
222
- 'champ couleur', 'color field', 'campo color', 'поле цвета', '颜色字段', '色フィールド',
223
- 'champ fichier', 'file field', 'campo archivo', 'поле файла', '文件字段', 'ファイルフィールド',
224
- 'champ cache', 'champ caché', 'hidden field', 'campo oculto', 'скрытое поле', '隐藏字段', '非表示フィールド',
225
- 'champ recherche', 'search field', 'campo busqueda', 'campo búsqueda', 'поле поиска', '搜索字段', '検索フィールド',
226
- 'champ url', 'url field', 'campo url', 'поле url', 'URL字段', 'URLフィールド',
227
- 'champ telephone', 'champ téléphone', 'phone field', 'campo telefono', 'campo teléfono', 'поле телефона', '电话字段', '電話フィールド',
228
- 'champ plage', 'range field', 'campo rango', 'поле диапазона', '范围字段', '範囲フィールド',
229
- 'bouton envoyer', 'submit button', 'boton enviar', 'botón enviar', 'кнопка отправки', '提交按钮', '送信ボタン',
230
- 'bouton reinitialiser', 'bouton réinitialiser', 'reset button', 'boton reiniciar', 'botón reiniciar', 'кнопка сброса', '重置按钮', 'リセットボタン'
231
- ]
232
- const lowerText = text.toLowerCase()
233
- return multiWordPatterns.some(p => lowerText.startsWith(p.toLowerCase()))
234
- }
235
-
236
- tokenize() {
237
- while (this.pos < this.source.length) {
238
- const loc = { line: this.line, column: this.column }
239
-
240
- if (this.peek() === '\n') {
241
- this.advance()
242
- if (this.pos < this.source.length && this.peek() !== '\n') {
243
- const indent = this.readIndent()
244
- this.tokens.push({ type: 'NEWLINE', loc })
245
- this.tokens.push({ type: 'INDENT', value: indent, loc: { line: this.line, column: 1 } })
246
- }
247
- continue
248
- }
249
-
250
- if (this.peek() === ' ' || this.peek() === '\t' || this.peek() === '\r') {
251
- this.skipWhitespaceInline()
252
- continue
253
- }
254
-
255
- if (this.peek() === '-' && this.peek(1) === '-' && this.peek(2) !== '-') {
256
- this.skipLineComment()
257
- continue
258
- }
259
-
260
- if (this.peek() === '{' && this.peek(1) === '-') {
261
- this.skipBlockComment()
262
- continue
263
- }
264
-
265
- const char = this.peek()
266
- const newLoc = { line: this.line, column: this.column }
267
-
268
- if (char === '"' || char === "'") {
269
- const value = this.readString(char)
270
- this.tokens.push({ type: 'STRING', value, quote: char, loc: newLoc })
271
- } else if (/[0-9]/.test(char) || (char === '-' && /[0-9]/.test(this.peek(1)))) {
272
- const value = this.readNumber()
273
- this.tokens.push({ type: 'NUMBER', value, loc: newLoc })
274
- } else if (char === '#') {
275
- this.advance()
276
- const id = this.readWord()
277
- this.tokens.push({ type: 'ID_SHORTHAND', value: id, loc: newLoc })
278
- } else if (char === '.') {
279
- this.advance()
280
- const className = this.readWord()
281
- this.tokens.push({ type: 'CLASS_SHORTHAND', value: className, loc: newLoc })
282
- } else if (char === '@') {
283
- this.advance()
284
- const directive = this.readWord()
285
- this.tokens.push({ type: 'DIRECTIVE', value: directive, loc: newLoc })
286
- } else if (char === '=') {
287
- this.advance()
288
- this.tokens.push({ type: 'EQUALS', loc: newLoc })
289
- } else if (char === ':') {
290
- this.advance()
291
- this.tokens.push({ type: 'COLON', loc: newLoc })
292
- } else if (char === ',') {
293
- this.advance()
294
- this.tokens.push({ type: 'COMMA', loc: newLoc })
295
- } else if (char === '(') {
296
- this.advance()
297
- this.tokens.push({ type: 'LPAREN', loc: newLoc })
298
- } else if (char === ')') {
299
- this.advance()
300
- this.tokens.push({ type: 'RPAREN', loc: newLoc })
301
- } else if (char === '[') {
302
- this.advance()
303
- this.tokens.push({ type: 'LBRACKET', loc: newLoc })
304
- } else if (char === ']') {
305
- this.advance()
306
- this.tokens.push({ type: 'RBRACKET', loc: newLoc })
307
- } else if (char === '{') {
308
- this.advance()
309
- if (this.peek() === '{') {
310
- this.advance()
311
- this.tokens.push({ type: 'EXPR_START', loc: newLoc })
312
- } else {
313
- this.tokens.push({ type: 'LBRACE', loc: newLoc })
314
- }
315
- } else if (char === '}') {
316
- this.advance()
317
- if (this.peek() === '}') {
318
- this.advance()
319
- this.tokens.push({ type: 'EXPR_END', loc: newLoc })
320
- } else {
321
- this.tokens.push({ type: 'RBRACE', loc: newLoc })
322
- }
323
- } else if (char === '/') {
324
- this.advance()
325
- this.tokens.push({ type: 'SLASH', loc: newLoc })
326
- } else if (char === '>') {
327
- this.advance()
328
- this.tokens.push({ type: 'GT', loc: newLoc })
329
- } else if (char === '<') {
330
- this.advance()
331
- this.tokens.push({ type: 'LT', loc: newLoc })
332
- } else if (/[a-zA-ZÀ-ÿА-яぁ-ゟァ-ヿ一-龯_]/.test(char)) {
333
- const word = this.readWord()
334
- const tokenType = this.classifyWord(word)
335
- this.tokens.push({ type: tokenType, value: word, loc: newLoc })
336
- } else {
337
- this.advance()
338
- }
339
- }
340
-
341
- this.tokens.push({ type: 'EOF', loc: { line: this.line, column: this.column } })
342
- return this.tokens
343
- }
344
-
345
- classifyWord(word) {
346
- const lowerWord = word.toLowerCase()
347
-
348
- if (this.isEtherKeyword(lowerWord)) return 'ETHER_KEYWORD'
349
- if (this.isElement(lowerWord)) return 'ELEMENT'
350
- if (this.isAttribute(lowerWord)) return 'ATTRIBUTE'
351
- if (this.isBooleanAttribute(lowerWord)) return 'BOOLEAN_ATTR'
352
-
353
- return 'IDENTIFIER'
354
- }
355
-
356
- isEtherKeyword(word) {
357
- const keywords = [
358
- 'si', 'if', 'sinon', 'else', 'sinonsi', 'elif',
359
- 'pour', 'for', 'dans', 'in', 'tantque', 'while',
360
- 'créer', 'create', 'ajouter', 'add',
361
- 'vers', 'to', 'nouvelle fenêtre', 'new window',
362
- 'même fenêtre', 'same window'
363
- ]
364
- return keywords.includes(word.toLowerCase())
365
- }
366
-
367
- isElement(word) {
368
- for (const category of Object.values(this.i18n)) {
369
- if (typeof category === 'object' && category !== null && !Array.isArray(category)) {
370
- for (const item of Object.values(category)) {
371
- if (typeof item === 'object' && item !== null && item.html) {
372
- for (const [lang, translation] of Object.entries(item)) {
373
- if (lang !== 'html' && typeof translation === 'string' &&
374
- translation.toLowerCase() === word.toLowerCase()) {
375
- return true
376
- }
377
- }
378
- }
379
- }
380
- }
381
- }
382
- return false
383
- }
384
-
385
- isAttribute(word) {
386
- const attrs = this.i18n.globalAttributes || {}
387
- for (const item of Object.values(attrs)) {
388
- if (typeof item === 'object' && item !== null) {
389
- for (const translation of Object.values(item)) {
390
- if (typeof translation === 'string' && translation.toLowerCase() === word.toLowerCase()) {
391
- return true
392
- }
393
- }
394
- }
395
- }
396
- return false
397
- }
398
-
399
- isBooleanAttribute(word) {
400
- const boolAttrs = [
401
- 'requis', 'required', 'requerido', 'обязательный', '必填', '必須',
402
- 'désactivé', 'disabled', 'desactivado', 'отключен', '禁用', '無効',
403
- 'coché', 'checked', 'marcado', 'отмечен', '选中', 'チェック済み',
404
- 'sélectionné', 'selected', 'seleccionado', 'выбран', '已选', '選択済み',
405
- 'lecture seule', 'readonly', 'solo lectura', 'только чтение', '只读', '読み取り専用',
406
- 'multiple', 'múltiple', 'множественный', '多选', '複数',
407
- 'autofocus', 'autocompléter', 'autocomplete',
408
- 'contrôles', 'controls', 'controles', 'управление', '控件', 'コントロール',
409
- 'lecture auto', 'autoplay', 'reproducción auto', 'автозапуск', '自动播放', '自動再生',
410
- 'muet', 'muted', 'silenciado', 'без звука', '静音', 'ミュート',
411
- 'en boucle', 'loop', 'bucle', 'цикл', '循环', 'ループ',
412
- 'ouvert', 'open', 'abierto', 'открыт', '打开', '開く',
413
- 'caché', 'hidden', 'oculto', 'скрытый', '隐藏', '非表示',
414
- 'novalidate', 'formnovalidate', 'async', 'defer', 'inert'
415
- ]
416
- return boolAttrs.includes(word.toLowerCase())
417
- }
418
- }
419
-
420
- class HTMLParser {
421
- constructor(i18nPath = null) {
422
- this.i18n = {}
423
- this.tokens = []
424
- this.pos = 0
425
- this.currentIndent = 0
426
-
427
- if (i18nPath) {
428
- this.loadI18n(i18nPath)
429
- }
430
- }
431
-
432
- loadI18n(filePath) {
433
- try {
434
- const content = fs.readFileSync(filePath, 'utf-8')
435
- this.i18n = JSON.parse(content)
436
- } catch (e) {
437
- console.error(`Erreur chargement i18n: ${e.message}`)
438
- this.i18n = {}
439
- }
440
- }
441
-
442
- setI18n(i18nData) {
443
- this.i18n = i18nData
444
- }
445
-
446
- parse(source) {
447
- const lexer = new HTMLLexer(source, this.i18n)
448
- this.tokens = lexer.tokenize()
449
- this.pos = 0
450
- return this.parseDocument()
451
- }
452
-
453
- peek(offset = 0) {
454
- const idx = this.pos + offset
455
- return idx < this.tokens.length ? this.tokens[idx] : { type: 'EOF' }
456
- }
457
-
458
- advance() {
459
- const token = this.tokens[this.pos]
460
- this.pos++
461
- return token
462
- }
463
-
464
- expect(type) {
465
- const token = this.peek()
466
- if (token.type !== type) {
467
- throw new Error(`Attendu ${type}, reçu ${token.type} à ligne ${token.loc?.line}`)
468
- }
469
- return this.advance()
470
- }
471
-
472
- match(...types) {
473
- return types.includes(this.peek().type)
474
- }
475
-
476
- skipNewlines() {
477
- while (this.match('NEWLINE', 'INDENT')) {
478
- if (this.match('INDENT')) {
479
- this.currentIndent = this.peek().value
480
- }
481
- this.advance()
482
- }
483
- }
484
-
485
- parseDocument() {
486
- let doctype = null
487
- let html = null
488
-
489
- this.skipNewlines()
490
-
491
- if (this.match('ELEMENT', 'IDENTIFIER')) {
492
- const word = this.peek().value.toLowerCase()
493
- if (word === 'document' || word === 'documento' || word === 'документ' || word === '文档' || word === 'ドキュメント') {
494
- this.advance()
495
- doctype = new AST.Doctype('html')
496
- this.skipNewlines()
497
- }
498
- }
499
-
500
- const children = []
501
- while (!this.match('EOF')) {
502
- const child = this.parseNode()
503
- if (child) {
504
- children.push(child)
505
- }
506
- this.skipNewlines()
507
- }
508
-
509
- if (children.length === 1 && children[0].type === 'Element' && children[0].htmlTag === 'html') {
510
- html = children[0]
511
- } else {
512
- html = new AST.Element('html', 'html', [], children)
513
- }
514
-
515
- return new AST.Document(doctype, html)
516
- }
517
-
518
- parseNode() {
519
- const token = this.peek()
520
-
521
- if (token.type === 'DIRECTIVE') {
522
- return this.parseDirective()
523
- }
524
-
525
- if (token.type === 'ETHER_KEYWORD') {
526
- return this.parseEtherConstruct()
527
- }
528
-
529
- if (token.type === 'ELEMENT' || token.type === 'IDENTIFIER') {
530
- return this.parseElement()
531
- }
532
-
533
- if (token.type === 'STRING') {
534
- return this.parseTextNode()
535
- }
536
-
537
- if (token.type === 'ID_SHORTHAND' || token.type === 'CLASS_SHORTHAND') {
538
- return this.parseShorthandElement()
539
- }
540
-
541
- if (token.type === 'EXPR_START') {
542
- return this.parseExpression()
543
- }
544
-
545
- this.advance()
546
- return null
547
- }
548
-
549
- parseElement() {
550
- const elemToken = this.advance()
551
- const tagName = elemToken.value
552
- const htmlTag = this.translateElement(tagName)
553
- const loc = elemToken.loc
554
- const elemIndent = loc.column - 1
555
-
556
- const attributes = this.parseAttributes()
557
-
558
- let textContent = null
559
- if (this.match('STRING')) {
560
- textContent = this.advance().value
561
- }
562
-
563
- const selfClosing = VOID_ELEMENTS.has(htmlTag)
564
-
565
- let children = []
566
- if (!selfClosing) {
567
- if (textContent) {
568
- children.push(new AST.TextNode(textContent))
569
- }
570
-
571
- const childElements = this.parseChildren(elemIndent)
572
- children = children.concat(childElements)
573
- }
574
-
575
- return new AST.Element(tagName, htmlTag, attributes, children, selfClosing, loc)
576
- }
577
-
578
- parseShorthandElement() {
579
- const firstToken = this.peek()
580
- const elemIndent = firstToken.loc ? firstToken.loc.column - 1 : 0
581
-
582
- const attributes = []
583
- let tagName = 'div'
584
- let htmlTag = 'div'
585
-
586
- while (this.match('ID_SHORTHAND', 'CLASS_SHORTHAND')) {
587
- if (this.match('ID_SHORTHAND')) {
588
- const idToken = this.advance()
589
- attributes.push(new AST.Attribute('id', 'id', idToken.value, idToken.loc))
590
- } else if (this.match('CLASS_SHORTHAND')) {
591
- const classToken = this.advance()
592
- const existing = attributes.find(a => a.htmlName === 'class')
593
- if (existing) {
594
- existing.value += ' ' + classToken.value
595
- } else {
596
- attributes.push(new AST.Attribute('classe', 'class', classToken.value, classToken.loc))
597
- }
598
- }
599
- }
600
-
601
- const moreAttrs = this.parseAttributes()
602
- attributes.push(...moreAttrs)
603
-
604
- let textContent = null
605
- if (this.match('STRING')) {
606
- textContent = this.advance().value
607
- }
608
-
609
- const children = []
610
- if (textContent) {
611
- children.push(new AST.TextNode(textContent))
612
- }
613
-
614
- const childElements = this.parseChildren(elemIndent)
615
- children.push(...childElements)
616
-
617
- return new AST.Element(tagName, htmlTag, attributes, children, false)
618
- }
619
-
620
- parseAttributes() {
621
- const attributes = []
622
-
623
- while (this.match('ATTRIBUTE', 'BOOLEAN_ATTR', 'IDENTIFIER', 'ID_SHORTHAND', 'CLASS_SHORTHAND')) {
624
- if (this.match('ID_SHORTHAND')) {
625
- const idToken = this.advance()
626
- attributes.push(new AST.Attribute('id', 'id', idToken.value, idToken.loc))
627
- continue
628
- }
629
-
630
- if (this.match('CLASS_SHORTHAND')) {
631
- const classToken = this.advance()
632
- const existing = attributes.find(a => a.htmlName === 'class')
633
- if (existing) {
634
- existing.value += ' ' + classToken.value
635
- } else {
636
- attributes.push(new AST.Attribute('classe', 'class', classToken.value, classToken.loc))
637
- }
638
- continue
639
- }
640
-
641
- if (this.match('BOOLEAN_ATTR')) {
642
- const boolToken = this.advance()
643
- const htmlAttr = this.translateAttribute(boolToken.value)
644
- attributes.push(new AST.BooleanAttribute(boolToken.value, htmlAttr, boolToken.loc))
645
- continue
646
- }
647
-
648
- const attrToken = this.advance()
649
- const attrName = attrToken.value
650
- const htmlAttr = this.translateAttribute(attrName)
651
-
652
- if (this.match('EQUALS')) {
653
- this.advance()
654
- let value = null
655
- if (this.match('STRING')) {
656
- value = this.advance().value
657
- } else if (this.match('NUMBER')) {
658
- value = String(this.advance().value)
659
- } else if (this.match('IDENTIFIER')) {
660
- value = this.advance().value
661
- }
662
- attributes.push(new AST.Attribute(attrName, htmlAttr, value, attrToken.loc))
663
- } else {
664
- attributes.push(new AST.BooleanAttribute(attrName, htmlAttr, attrToken.loc))
665
- }
666
- }
667
-
668
- return attributes
669
- }
670
-
671
- parseChildren(parentIndent = -1) {
672
- const children = []
673
-
674
- while (!this.match('EOF')) {
675
- this.skipNewlines()
676
-
677
- if (this.currentIndent <= parentIndent) {
678
- break
679
- }
680
-
681
- if (this.match('ELEMENT', 'IDENTIFIER', 'STRING', 'ID_SHORTHAND', 'CLASS_SHORTHAND', 'ETHER_KEYWORD', 'DIRECTIVE', 'EXPR_START')) {
682
- const child = this.parseNode()
683
- if (child) {
684
- children.push(child)
685
- }
686
- } else {
687
- break
688
- }
689
- }
690
-
691
- return children
692
- }
693
-
694
- parseTextNode() {
695
- const strToken = this.advance()
696
- return new AST.TextNode(strToken.value, strToken.loc)
697
- }
698
-
699
- parseExpression() {
700
- this.expect('EXPR_START')
701
- let expr = ''
702
- while (!this.match('EXPR_END', 'EOF')) {
703
- expr += this.advance().value || ''
704
- }
705
- if (this.match('EXPR_END')) {
706
- this.advance()
707
- }
708
- return new AST.EtherExpression(expr.trim())
709
- }
710
-
711
- parseDirective() {
712
- const dirToken = this.advance()
713
- const name = dirToken.value.toLowerCase()
714
-
715
- let value = null
716
- if (this.match('STRING', 'IDENTIFIER', 'NUMBER')) {
717
- value = this.advance().value
718
- }
719
-
720
- return new AST.EtherDirective(name, value, dirToken.loc)
721
- }
722
-
723
- parseEtherConstruct() {
724
- const keyword = this.peek().value.toLowerCase()
725
-
726
- if (['si', 'if'].includes(keyword)) {
727
- return this.parseConditional()
728
- }
729
-
730
- if (['pour', 'for'].includes(keyword)) {
731
- return this.parseLoop()
732
- }
733
-
734
- this.advance()
735
- return null
736
- }
737
-
738
- parseConditional() {
739
- this.advance()
740
-
741
- let condition = ''
742
- while (!this.match('NEWLINE', 'EOF')) {
743
- condition += (this.advance().value || '') + ' '
744
- }
745
- condition = condition.trim()
746
-
747
- const thenBranch = this.parseChildren()
748
- let elseBranch = null
749
-
750
- this.skipNewlines()
751
- if (this.match('ETHER_KEYWORD')) {
752
- const word = this.peek().value.toLowerCase()
753
- if (['sinon', 'else'].includes(word)) {
754
- this.advance()
755
- elseBranch = this.parseChildren()
756
- }
757
- }
758
-
759
- return new AST.EtherConditional(condition, thenBranch, elseBranch)
760
- }
761
-
762
- parseLoop() {
763
- this.advance()
764
-
765
- let variable = ''
766
- if (this.match('IDENTIFIER')) {
767
- variable = this.advance().value
768
- }
769
-
770
- if (this.match('ETHER_KEYWORD', 'IDENTIFIER')) {
771
- const word = this.peek().value.toLowerCase()
772
- if (['dans', 'in'].includes(word)) {
773
- this.advance()
774
- }
775
- }
776
-
777
- let iterable = ''
778
- while (!this.match('NEWLINE', 'EOF')) {
779
- iterable += (this.advance().value || '') + ' '
780
- }
781
- iterable = iterable.trim()
782
-
783
- const children = this.parseChildren()
784
-
785
- return new AST.EtherLoop(variable, iterable, children)
786
- }
787
-
788
- translateElement(name) {
789
- const lowerName = name.toLowerCase()
790
-
791
- for (const category of Object.values(this.i18n)) {
792
- if (typeof category === 'object' && category !== null && !Array.isArray(category)) {
793
- for (const item of Object.values(category)) {
794
- if (typeof item === 'object' && item !== null && item.html) {
795
- for (const [lang, translation] of Object.entries(item)) {
796
- if (lang !== 'html' && typeof translation === 'string' &&
797
- translation.toLowerCase() === lowerName) {
798
- return item.html.replace(/<|>/g, '')
799
- }
800
- }
801
- }
802
- }
803
- }
804
- }
805
-
806
- const directMap = {
807
- 'document': 'html', 'documento': 'html', 'документ': 'html', '文档': 'html', 'ドキュメント': 'html',
808
- 'tete': 'head', 'tête': 'head', 'head': 'head', 'cabeza': 'head', 'голова': 'head', '头部': 'head', 'ヘッド': 'head',
809
- 'corps': 'body', 'body': 'body', 'cuerpo': 'body', 'тело': 'body', '主体': 'body', 'ボディ': 'body',
810
- 'titre': 'title', 'title': 'title', 'título': 'title', 'titulo': 'title', 'заголовок': 'title', '标题': 'title', 'タイトル': 'title',
811
- 'titre 1': 'h1', 'titre1': 'h1', 'heading 1': 'h1', 'título 1': 'h1', 'titulo 1': 'h1', 'заголовок 1': 'h1', '标题 1': 'h1', '見出し 1': 'h1',
812
- 'titre 2': 'h2', 'titre2': 'h2', 'heading 2': 'h2', 'título 2': 'h2', 'titulo 2': 'h2', 'заголовок 2': 'h2', '标题 2': 'h2', '見出し 2': 'h2',
813
- 'titre 3': 'h3', 'titre3': 'h3', 'heading 3': 'h3', 'título 3': 'h3', 'titulo 3': 'h3', 'заголовок 3': 'h3', '标题 3': 'h3', '見出し 3': 'h3',
814
- 'titre 4': 'h4', 'titre4': 'h4', 'heading 4': 'h4', 'título 4': 'h4', 'titulo 4': 'h4', 'заголовок 4': 'h4', '标题 4': 'h4', '見出し 4': 'h4',
815
- 'titre 5': 'h5', 'titre5': 'h5', 'heading 5': 'h5', 'título 5': 'h5', 'titulo 5': 'h5', 'заголовок 5': 'h5', '标题 5': 'h5', '見出し 5': 'h5',
816
- 'titre 6': 'h6', 'titre6': 'h6', 'heading 6': 'h6', 'título 6': 'h6', 'titulo 6': 'h6', 'заголовок 6': 'h6', '标题 6': 'h6', '見出し 6': 'h6',
817
- 'paragraphe': 'p', 'paragraph': 'p', 'párrafo': 'p', 'parrafo': 'p', 'параграф': 'p', '段落': 'p',
818
- 'division': 'div', 'div': 'div', 'bloque': 'div', 'блок': 'div', '块': 'div', 'ディブ': 'div',
819
- 'etendue': 'span', 'étendue': 'span', 'span': 'span', 'extensión': 'span', 'extension': 'span', 'спан': 'span', '跨度': 'span', 'スパン': 'span',
820
- 'lien': 'a', 'anchor': 'a', 'link': 'a', 'enlace': 'a', 'ссылка': 'a', '链接': 'a', 'リンク': 'a',
821
- 'image': 'img', 'img': 'img', 'imagen': 'img', 'изображение': 'img', '图片': 'img', '画像': 'img',
822
- 'liste ordonnee': 'ol', 'liste ordonnée': 'ol', 'ordered list': 'ol', 'lista ordenada': 'ol', 'упорядоченный список': 'ol', '有序列表': 'ol', '順序付きリスト': 'ol',
823
- 'liste non ordonnee': 'ul', 'liste non ordonnée': 'ul', 'unordered list': 'ul', 'lista no ordenada': 'ul', 'неупорядоченный список': 'ul', '无序列表': 'ul', '順序なしリスト': 'ul',
824
- 'element liste': 'li', 'élément liste': 'li', 'list item': 'li', 'elemento lista': 'li', 'элемент списка': 'li', '列表项': 'li', 'リスト項目': 'li',
825
- 'tableau': 'table', 'table': 'table', 'tabla': 'table', 'таблица': 'table', '表格': 'table', 'テーブル': 'table',
826
- 'ligne': 'tr', 'row': 'tr', 'fila': 'tr', 'строка': 'tr', '行': 'tr',
827
- 'cellule': 'td', 'cell': 'td', 'celda': 'td', 'ячейка': 'td', '单元格': 'td', 'セル': 'td',
828
- 'cellule entete': 'th', 'cellule entête': 'th', 'header cell': 'th', 'celda encabezado': 'th', 'ячейка заголовка': 'th', '表头单元格': 'th', 'ヘッダーセル': 'th',
829
- 'entete': 'header', 'entête': 'header', 'header': 'header', 'encabezado': 'header', 'шапка': 'header', '头部': 'header', 'ヘッダー': 'header',
830
- 'pied': 'footer', 'footer': 'footer', 'pie': 'footer', 'подвал': 'footer', '页脚': 'footer', 'フッター': 'footer',
831
- 'navigation': 'nav', 'nav': 'nav', 'navegación': 'nav', 'navegacion': 'nav', 'навигация': 'nav', '导航': 'nav', 'ナビ': 'nav',
832
- 'principal': 'main', 'main': 'main', 'основной': 'main', '主要': 'main', 'メイン': 'main',
833
- 'section': 'section', 'sección': 'section', 'seccion': 'section', 'секция': 'section', '部分': 'section', 'セクション': 'section',
834
- 'article': 'article', 'artículo': 'article', 'articulo': 'article', 'статья': 'article', '文章': 'article', '記事': 'article',
835
- 'aside': 'aside', 'aparte': 'aside', 'боковой': 'aside', '侧边': 'aside', 'アサイド': 'aside',
836
- 'formulaire': 'form', 'form': 'form', 'formulario': 'form', 'форма': 'form', '表单': 'form', 'フォーム': 'form',
837
- 'entree': 'input', 'entrée': 'input', 'input': 'input', 'entrada': 'input', 'ввод': 'input', '输入': 'input', '入力': 'input',
838
- 'champ': 'input', 'field': 'input', 'campo': 'input', 'поле': 'input', '字段': 'input', 'フィールド': 'input',
839
- 'champ texte': 'input', 'text field': 'input', 'campo texto': 'input', 'текстовое поле': 'input', '文本字段': 'input', 'テキストフィールド': 'input',
840
- 'champ email': 'input', 'email field': 'input', 'campo email': 'input', 'поле email': 'input', '邮箱字段': 'input', 'メールフィールド': 'input',
841
- 'champ mot de passe': 'input', 'password field': 'input', 'campo contraseña': 'input', 'campo contrasena': 'input', 'поле пароля': 'input', '密码字段': 'input', 'パスワードフィールド': 'input',
842
- 'champ numero': 'input', 'champ numéro': 'input', 'number field': 'input', 'campo numero': 'input', 'campo número': 'input', 'числовое поле': 'input', '数字字段': 'input', '数値フィールド': 'input',
843
- 'champ date': 'input', 'date field': 'input', 'campo fecha': 'input', 'поле даты': 'input', '日期字段': 'input', '日付フィールド': 'input',
844
- 'champ heure': 'input', 'time field': 'input', 'campo hora': 'input', 'поле времени': 'input', '时间字段': 'input', '時間フィールド': 'input',
845
- 'champ couleur': 'input', 'color field': 'input', 'campo color': 'input', 'поле цвета': 'input', '颜色字段': 'input', '色フィールド': 'input',
846
- 'champ fichier': 'input', 'file field': 'input', 'campo archivo': 'input', 'поле файла': 'input', '文件字段': 'input', 'ファイルフィールド': 'input',
847
- 'champ cache': 'input', 'champ caché': 'input', 'hidden field': 'input', 'campo oculto': 'input', 'скрытое поле': 'input', '隐藏字段': 'input', '非表示フィールド': 'input',
848
- 'champ recherche': 'input', 'search field': 'input', 'campo busqueda': 'input', 'campo búsqueda': 'input', 'поле поиска': 'input', '搜索字段': 'input', '検索フィールド': 'input',
849
- 'champ url': 'input', 'url field': 'input', 'campo url': 'input', 'поле url': 'input', 'URL字段': 'input', 'URLフィールド': 'input',
850
- 'champ telephone': 'input', 'champ téléphone': 'input', 'phone field': 'input', 'campo telefono': 'input', 'campo teléfono': 'input', 'поле телефона': 'input', '电话字段': 'input', '電話フィールド': 'input',
851
- 'champ plage': 'input', 'range field': 'input', 'campo rango': 'input', 'поле диапазона': 'input', '范围字段': 'input', '範囲フィールド': 'input',
852
- 'case a cocher': 'input', 'case à cocher': 'input', 'checkbox': 'input', 'casilla verificacion': 'input', 'casilla verificación': 'input', 'флажок': 'input', '复选框': 'input', 'チェックボックス': 'input',
853
- 'bouton radio': 'input', 'radio button': 'input', 'boton radio': 'input', 'botón radio': 'input', 'радиокнопка': 'input', '单选按钮': 'input', 'ラジオボタン': 'input',
854
- 'bouton': 'button', 'button': 'button', 'botón': 'button', 'boton': 'button', 'кнопка': 'button', '按钮': 'button', 'ボタン': 'button',
855
- 'bouton envoyer': 'button', 'submit button': 'button', 'boton enviar': 'button', 'botón enviar': 'button', 'кнопка отправки': 'button', '提交按钮': 'button', '送信ボタン': 'button',
856
- 'bouton reinitialiser': 'button', 'bouton réinitialiser': 'button', 'reset button': 'button', 'boton reiniciar': 'button', 'botón reiniciar': 'button', 'кнопка сброса': 'button', '重置按钮': 'button', 'リセットボタン': 'button',
857
- 'zone texte': 'textarea', 'textarea': 'textarea', 'area texto': 'textarea', 'área texto': 'textarea', 'текстовая область': 'textarea', '文本区域': 'textarea', 'テキストエリア': 'textarea',
858
- 'selection': 'select', 'sélection': 'select', 'select': 'select', 'selección': 'select', 'seleccion': 'select', 'выбор': 'select', '选择': 'select', 'セレクト': 'select',
859
- 'liste deroulante': 'select', 'liste déroulante': 'select', 'dropdown': 'select', 'lista desplegable': 'select', 'выпадающий список': 'select', '下拉列表': 'select', 'ドロップダウン': 'select',
860
- 'option': 'option', 'opción': 'option', 'opcion': 'option', 'опция': 'option', '选项': 'option', 'オプション': 'option',
861
- 'groupe options': 'optgroup', 'option group': 'optgroup', 'grupo opciones': 'optgroup', 'группа опций': 'optgroup', '选项组': 'optgroup', 'オプショングループ': 'optgroup',
862
- 'etiquette': 'label', 'étiquette': 'label', 'label': 'label', 'etiqueta': 'label', 'метка': 'label', '标签': 'label', 'ラベル': 'label',
863
- 'groupe champs': 'fieldset', 'fieldset': 'fieldset', 'grupo campos': 'fieldset', 'группа полей': 'fieldset', '字段组': 'fieldset', 'フィールドセット': 'fieldset',
864
- 'legende': 'legend', 'légende': 'legend', 'legend': 'legend', 'leyenda': 'legend', 'легенда': 'legend', '图例': 'legend', '凡例': 'legend',
865
- 'liste donnees': 'datalist', 'liste données': 'datalist', 'datalist': 'datalist', 'lista datos': 'datalist', 'список данных': 'datalist', '数据列表': 'datalist', 'データリスト': 'datalist',
866
- 'sortie': 'output', 'output': 'output', 'salida': 'output', 'вывод': 'output', '输出': 'output', '出力': 'output',
867
- 'progression': 'progress', 'progress': 'progress', 'progreso': 'progress', 'прогресс': 'progress', '进度': 'progress', 'プログレス': 'progress',
868
- 'metre': 'meter', 'mètre': 'meter', 'meter': 'meter', 'medidor': 'meter', 'метр': 'meter', '计量器': 'meter', 'メーター': 'meter',
869
- 'video': 'video', 'vidéo': 'video', 'vídeo': 'video', 'видео': 'video', '视频': 'video', 'ビデオ': 'video',
870
- 'audio': 'audio', 'аудио': 'audio', '音频': 'audio', 'オーディオ': 'audio',
871
- 'source': 'source', 'fuente': 'source', 'источник': 'source', '来源': 'source', 'ソース': 'source',
872
- 'piste': 'track', 'track': 'track', 'дорожка': 'track', '轨道': 'track', 'トラック': 'track',
873
- 'canevas': 'canvas', 'canvas': 'canvas', 'lienzo': 'canvas', 'холст': 'canvas', '画布': 'canvas', 'キャンバス': 'canvas',
874
- 'svg': 'svg',
875
- 'math': 'math', 'mathématiques': 'math', 'matematicas': 'math', 'matemáticas': 'math', 'математика': 'math', '数学': 'math',
876
- 'cadre en ligne': 'iframe', 'iframe': 'iframe', 'inline frame': 'iframe', 'marco en linea': 'iframe', 'встроенный фрейм': 'iframe', '内联框架': 'iframe', 'インラインフレーム': 'iframe',
877
- 'objet': 'object', 'object': 'object', 'objeto': 'object', 'объект': 'object', '对象': 'object', 'オブジェクト': 'object',
878
- 'incorporation': 'embed', 'embed': 'embed', 'incrustar': 'embed', 'встраивание': 'embed', '嵌入': 'embed', '埋め込み': 'embed',
879
- 'script': 'script', 'скрипт': 'script', '脚本': 'script', 'スクリプト': 'script',
880
- 'noscript': 'noscript',
881
- 'style': 'style', 'estilo': 'style', 'стиль': 'style', '样式': 'style', 'スタイル': 'style',
882
- 'lien css': 'link', 'link': 'link',
883
- 'meta': 'meta', 'méta': 'meta', 'мета': 'meta', '元': 'meta', 'メタ': 'meta',
884
- 'base': 'base',
885
- 'retour ligne': 'br', 'line break': 'br', 'salto linea': 'br', 'salto línea': 'br', 'перенос строки': 'br', '换行': 'br', '改行': 'br',
886
- 'ligne horizontale': 'hr', 'horizontal rule': 'hr', 'linea horizontal': 'hr', 'línea horizontal': 'hr', 'горизонтальная линия': 'hr', '水平线': 'hr', '水平線': 'hr',
887
- 'gras': 'b', 'bold': 'b', 'negrita': 'b', 'жирный': 'b', '粗体': 'b', '太字': 'b',
888
- 'italique': 'i', 'italic': 'i', 'cursiva': 'i', 'курсив': 'i', '斜体': 'i', 'イタリック': 'i',
889
- 'souligne': 'u', 'souligné': 'u', 'underline': 'u', 'subrayado': 'u', 'подчеркнутый': 'u', '下划线': 'u', '下線': 'u',
890
- 'barre': 's', 'barré': 's', 'strikethrough': 's', 'tachado': 's', 'зачеркнутый': 's', '删除线': 's', '取り消し線': 's',
891
- 'fort': 'strong', 'strong': 'strong', 'fuerte': 'strong', 'важный': 'strong', '强调': 'strong', '強調': 'strong',
892
- 'emphase': 'em', 'emphasis': 'em', 'énfasis': 'em', 'enfasis': 'em', 'акцент': 'em', '着重': 'em', '強調': 'em',
893
- 'marque': 'mark', 'mark': 'mark', 'marca': 'mark', 'маркер': 'mark', '标记': 'mark', 'マーク': 'mark',
894
- 'petit': 'small', 'small': 'small', 'pequeño': 'small', 'pequeno': 'small', 'маленький': 'small', '小': 'small', '小さい': 'small',
895
- 'supprime': 'del', 'supprimé': 'del', 'deleted': 'del', 'eliminado': 'del', 'удаленный': 'del', '删除': 'del', '削除': 'del',
896
- 'insere': 'ins', 'inséré': 'ins', 'inserted': 'ins', 'insertado': 'ins', 'вставленный': 'ins', '插入': 'ins', '挿入': 'ins',
897
- 'indice': 'sub', 'subscript': 'sub', 'subíndice': 'sub', 'subindice': 'sub', 'подстрочный': 'sub', '下标': 'sub', '下付き': 'sub',
898
- 'exposant': 'sup', 'superscript': 'sup', 'superíndice': 'sup', 'superindice': 'sup', 'надстрочный': 'sup', '上标': 'sup', '上付き': 'sup',
899
- 'code': 'code', 'código': 'code', 'codigo': 'code', 'код': 'code', '代码': 'code', 'コード': 'code',
900
- 'preformate': 'pre', 'préformaté': 'pre', 'preformatted': 'pre', 'preformateado': 'pre', 'предформатированный': 'pre', '预格式化': 'pre', '整形済み': 'pre',
901
- 'variable': 'var', 'var': 'var', 'переменная': 'var', '变量': 'var', '変数': 'var',
902
- 'echantillon': 'samp', 'échantillon': 'samp', 'sample': 'samp', 'muestra': 'samp', 'образец': 'samp', '示例': 'samp', 'サンプル': 'samp',
903
- 'clavier': 'kbd', 'keyboard': 'kbd', 'teclado': 'kbd', 'клавиатура': 'kbd', '键盘': 'kbd', 'キーボード': 'kbd',
904
- 'citation': 'q', 'quote': 'q', 'cita': 'q', 'цитата': 'q', '引用': 'q', '引用符': 'q',
905
- 'citation bloc': 'blockquote', 'blockquote': 'blockquote', 'cita bloque': 'blockquote', 'блок цитаты': 'blockquote', '块引用': 'blockquote', 'ブロック引用': 'blockquote',
906
- 'abbreviation': 'abbr', 'abreviatura': 'abbr', 'abbr': 'abbr', 'аббревиатура': 'abbr', '缩写': 'abbr', '略語': 'abbr',
907
- 'adresse': 'address', 'address': 'address', 'dirección': 'address', 'direccion': 'address', 'адрес': 'address', '地址': 'address', 'アドレス': 'address',
908
- 'date temps': 'time', 'time': 'time', 'tiempo': 'time', 'время': 'time', '时间': 'time', '時間': 'time',
909
- 'definition': 'dfn', 'définition': 'dfn', 'dfn': 'dfn', 'definición': 'dfn', 'определение': 'dfn', '定义': 'dfn', '定義': 'dfn',
910
- 'figure': 'figure', 'figura': 'figure', 'рисунок': 'figure', '图': 'figure', '図': 'figure',
911
- 'legende figure': 'figcaption', 'légende figure': 'figcaption', 'figure caption': 'figcaption', 'leyenda figura': 'figcaption', 'подпись рисунка': 'figcaption', '图片说明': 'figcaption', '図のキャプション': 'figcaption',
912
- 'details': 'details', 'détails': 'details', 'detalles': 'details', 'детали': 'details', '详情': 'details', '詳細': 'details',
913
- 'resume': 'summary', 'résumé': 'summary', 'summary': 'summary', 'resumen': 'summary', 'итог': 'summary', '摘要': 'summary', '概要': 'summary',
914
- 'dialogue': 'dialog', 'dialog': 'dialog', 'diálogo': 'dialog', 'dialogo': 'dialog', 'диалог': 'dialog', '对话框': 'dialog', 'ダイアログ': 'dialog',
915
- 'modele': 'template', 'modèle': 'template', 'template': 'template', 'plantilla': 'template', 'шаблон': 'template', '模板': 'template', 'テンプレート': 'template',
916
- 'emplacement': 'slot', 'slot': 'slot', 'ranura': 'slot', 'слот': 'slot', '插槽': 'slot', 'スロット': 'slot',
917
- 'carte': 'map', 'map': 'map', 'mapa': 'map', 'карта': 'map', '地图': 'map', 'マップ': 'map',
918
- 'zone': 'area', 'area': 'area', 'область': 'area', '区域': 'area', 'エリア': 'area',
919
- 'liste description': 'dl', 'description list': 'dl', 'lista descripcion': 'dl', 'lista descripción': 'dl', 'список описания': 'dl', '描述列表': 'dl', '説明リスト': 'dl',
920
- 'terme': 'dt', 'term': 'dt', 'término': 'dt', 'termino': 'dt', 'термин': 'dt', '术语': 'dt', '用語': 'dt',
921
- 'description': 'dd', 'descripción': 'dd', 'descripcion': 'dd', 'описание': 'dd', '描述': 'dd', '説明': 'dd',
922
- 'entete tableau': 'thead', 'entête tableau': 'thead', 'table header': 'thead', 'encabezado tabla': 'thead', 'заголовок таблицы': 'thead', '表头': 'thead', 'テーブルヘッダー': 'thead',
923
- 'corps tableau': 'tbody', 'table body': 'tbody', 'cuerpo tabla': 'tbody', 'тело таблицы': 'tbody', '表体': 'tbody', 'テーブル本体': 'tbody',
924
- 'pied tableau': 'tfoot', 'table footer': 'tfoot', 'pie tabla': 'tfoot', 'подвал таблицы': 'tfoot', '表脚': 'tfoot', 'テーブルフッター': 'tfoot',
925
- 'groupe colonnes': 'colgroup', 'column group': 'colgroup', 'grupo columnas': 'colgroup', 'группа колонок': 'colgroup', '列组': 'colgroup', '列グループ': 'colgroup',
926
- 'colonne': 'col', 'column': 'col', 'columna': 'col', 'колонка': 'col', '列': 'col',
927
- 'legende tableau': 'caption', 'légende tableau': 'caption', 'table caption': 'caption', 'leyenda tabla': 'caption', 'подпись таблицы': 'caption', '表格标题': 'caption', 'テーブルキャプション': 'caption',
928
- 'image reactive': 'picture', 'image réactive': 'picture', 'picture': 'picture', 'imagen reactiva': 'picture', 'адаптивное изображение': 'picture', '响应式图片': 'picture', 'レスポンシブ画像': 'picture',
929
- 'ruby': 'ruby', 'рубин': 'ruby', '注音': 'ruby', 'ルビ': 'ruby',
930
- 'annotation ruby': 'rt', 'ruby text': 'rt', 'anotacion ruby': 'rt', 'anotación ruby': 'rt', 'аннотация ruby': 'rt', 'ruby注释': 'rt', 'ルビ注釈': 'rt',
931
- 'parentheses ruby': 'rp', 'parenthèses ruby': 'rp', 'ruby parenthesis': 'rp', 'paréntesis ruby': 'rp', 'скобки ruby': 'rp', 'ruby括号': 'rp', 'ルビ括弧': 'rp',
932
- 'isolation bidi': 'bdi', 'bidi isolation': 'bdi', 'aislamiento bidi': 'bdi', 'изоляция bidi': 'bdi', '双向隔离': 'bdi', '双方向分離': 'bdi',
933
- 'remplacement bidi': 'bdo', 'bidi override': 'bdo', 'reemplazo bidi': 'bdo', 'переопределение bidi': 'bdo', '双向覆盖': 'bdo', '双方向オーバーライド': 'bdo',
934
- 'retour ligne possible': 'wbr', 'word break': 'wbr', 'wbr': 'wbr', 'oportunidad salto': 'wbr', 'возможность переноса': 'wbr', '换行机会': 'wbr', '改行可能': 'wbr',
935
- 'donnees': 'data', 'données': 'data', 'data': 'data', 'datos': 'data', 'данные': 'data', '数据': 'data', 'データ': 'data'
936
- }
937
-
938
- return directMap[lowerName] || name
939
- }
940
-
941
- translateAttribute(name) {
942
- const lowerName = name.toLowerCase()
943
-
944
- const attrs = this.i18n.globalAttributes || {}
945
- for (const item of Object.values(attrs)) {
946
- if (typeof item === 'object' && item !== null && item.html) {
947
- for (const [lang, translation] of Object.entries(item)) {
948
- if (lang !== 'html' && typeof translation === 'string' &&
949
- translation.toLowerCase() === lowerName) {
950
- return item.html
951
- }
952
- }
953
- }
954
- }
955
-
956
- const directMap = {
957
- 'id': 'id', 'identifiant': 'id', 'identificador': 'id', 'идентификатор': 'id', '标识': 'id', '識別子': 'id',
958
- 'classe': 'class', 'class': 'class', 'clase': 'class', 'класс': 'class', '类': 'class', 'クラス': 'class',
959
- 'style': 'style', 'estilo': 'style', 'стиль': 'style', '样式': 'style', 'スタイル': 'style',
960
- 'titre': 'title', 'title': 'title', 'título': 'title', 'titulo': 'title', 'заголовок': 'title', '标题': 'title', 'タイトル': 'title',
961
- 'source': 'src', 'src': 'src', 'fuente': 'src', 'источник': 'src', '来源': 'src', 'ソース': 'src',
962
- 'alternative': 'alt', 'alt': 'alt', 'alternativa': 'alt', 'альтернатива': 'alt', '替代': 'alt', '代替': 'alt',
963
- 'largeur': 'width', 'width': 'width', 'ancho': 'width', 'ширина': 'width', '宽度': 'width', '幅': 'width',
964
- 'hauteur': 'height', 'height': 'height', 'alto': 'height', 'высота': 'height', '高度': 'height', '高さ': 'height',
965
- 'href': 'href', 'lien': 'href', 'enlace': 'href', 'ссылка': 'href', '链接': 'href', 'リンク': 'href',
966
- 'cible': 'target', 'target': 'target', 'objetivo': 'target', 'цель': 'target', '目标': 'target', 'ターゲット': 'target',
967
- 'type': 'type', 'tipo': 'type', 'тип': 'type', '类型': 'type', 'タイプ': 'type',
968
- 'nom': 'name', 'name': 'name', 'nombre': 'name', 'имя': 'name', '名称': 'name', '名前': 'name',
969
- 'valeur': 'value', 'value': 'value', 'valor': 'value', 'значение': 'value', '值': 'value', '値': 'value',
970
- 'placeholder': 'placeholder', 'marcador': 'placeholder', 'заполнитель': 'placeholder', '占位符': 'placeholder', 'プレースホルダー': 'placeholder',
971
- 'action': 'action', 'acción': 'action', 'accion': 'action', 'действие': 'action', '动作': 'action', 'アクション': 'action',
972
- 'methode': 'method', 'méthode': 'method', 'method': 'method', 'método': 'method', 'metodo': 'method', 'метод': 'method', '方法': 'method', 'メソッド': 'method',
973
- 'pour': 'for', 'for': 'for', 'para': 'for', 'для': 'for', '为': 'for', 'フォー': 'for',
974
- 'donnees': 'data', 'données': 'data', 'data': 'data', 'datos': 'data', 'данные': 'data', '数据': 'data', 'データ': 'data',
975
- 'role': 'role', 'rôle': 'role', 'rol': 'role', 'роль': 'role', '角色': 'role', 'ロール': 'role',
976
- 'tabindex': 'tabindex', 'indice tab': 'tabindex', 'índice tab': 'tabindex', 'индекс tab': 'tabindex', 'Tab索引': 'tabindex', 'タブインデックス': 'tabindex',
977
- 'lang': 'lang', 'langue': 'lang', 'idioma': 'lang', 'язык': 'lang', '语言': 'lang', '言語': 'lang',
978
- 'dir': 'dir', 'direction': 'dir', 'dirección': 'dir', 'direccion': 'dir', 'направление': 'dir', '方向': 'dir',
979
- 'rel': 'rel', 'relation': 'rel', 'relación': 'rel', 'relacion': 'rel', 'отношение': 'rel', '关系': 'rel', '関係': 'rel',
980
- 'media': 'media', 'média': 'media', 'медиа': 'media', '媒体': 'media', 'メディア': 'media',
981
- 'contenu': 'content', 'content': 'content', 'contenido': 'content', 'содержимое': 'content', '内容': 'content', 'コンテンツ': 'content',
982
- 'charset': 'charset', 'encodage': 'charset', 'codificación': 'charset', 'codificacion': 'charset', 'кодировка': 'charset', '字符集': 'charset', '文字セット': 'charset',
983
- 'colonnes': 'cols', 'cols': 'cols', 'columnas': 'cols', 'колонки': 'cols', '列数': 'cols',
984
- 'lignes': 'rows', 'rows': 'rows', 'filas': 'rows', 'строки': 'rows', '行数': 'rows',
985
- 'fusion colonnes': 'colspan', 'colspan': 'colspan', 'combinacion columnas': 'colspan', 'объединение колонок': 'colspan', '合并列': 'colspan', '列結合': 'colspan',
986
- 'fusion lignes': 'rowspan', 'rowspan': 'rowspan', 'combinacion filas': 'rowspan', 'объединение строк': 'rowspan', '合并行': 'rowspan', '行結合': 'rowspan',
987
- 'min': 'min', 'minimum': 'min', 'mínimo': 'min', 'minimo': 'min', 'минимум': 'min', '最小': 'min',
988
- 'max': 'max', 'maximum': 'max', 'máximo': 'max', 'maximo': 'max', 'максимум': 'max', '最大': 'max',
989
- 'pas': 'step', 'step': 'step', 'paso': 'step', 'шаг': 'step', '步长': 'step', 'ステップ': 'step',
990
- 'motif': 'pattern', 'pattern': 'pattern', 'patrón': 'pattern', 'patron': 'pattern', 'шаблон': 'pattern', '模式': 'pattern', 'パターン': 'pattern',
991
- 'requis': 'required', 'required': 'required', 'requerido': 'required', 'обязательный': 'required', '必填': 'required', '必須': 'required',
992
- 'obligatoire': 'required', 'obligatorio': 'required',
993
- 'desactive': 'disabled', 'désactivé': 'disabled', 'disabled': 'disabled', 'desactivado': 'disabled', 'отключенный': 'disabled', '禁用': 'disabled', '無効': 'disabled',
994
- 'inactif': 'disabled', 'inactivo': 'disabled',
995
- 'coche': 'checked', 'coché': 'checked', 'checked': 'checked', 'marcado': 'checked', 'отмеченный': 'checked', '选中': 'checked', 'チェック済み': 'checked',
996
- 'selectionne': 'selected', 'sélectionné': 'selected', 'selected': 'selected', 'seleccionado': 'selected', 'выбранный': 'selected', '已选择': 'selected', '選択済み': 'selected',
997
- 'lecture seule': 'readonly', 'readonly': 'readonly', 'solo lectura': 'readonly', 'только чтение': 'readonly', '只读': 'readonly', '読み取り専用': 'readonly',
998
- 'multiple': 'multiple', 'múltiple': 'multiple', 'множественный': 'multiple', '多个': 'multiple', '複数': 'multiple',
999
- 'autofocus': 'autofocus', 'focus auto': 'autofocus', 'enfoque automatico': 'autofocus', 'enfoque automático': 'autofocus', 'автофокус': 'autofocus', '自动聚焦': 'autofocus', '自動フォーカス': 'autofocus',
1000
- 'autocompleter': 'autocomplete', 'autocompléter': 'autocomplete', 'autocomplete': 'autocomplete', 'autocompletar': 'autocomplete', 'автозаполнение': 'autocomplete', '自动完成': 'autocomplete', 'オートコンプリート': 'autocomplete',
1001
- 'controles': 'controls', 'contrôles': 'controls', 'controls': 'controls', 'управление': 'controls', '控件': 'controls', 'コントロール': 'controls',
1002
- 'lecture auto': 'autoplay', 'autoplay': 'autoplay', 'reproduccion automatica': 'autoplay', 'reproducción automática': 'autoplay', 'автовоспроизведение': 'autoplay', '自动播放': 'autoplay', '自動再生': 'autoplay',
1003
- 'muet': 'muted', 'muted': 'muted', 'silenciado': 'muted', 'без звука': 'muted', '静音': 'muted', 'ミュート': 'muted',
1004
- 'en boucle': 'loop', 'loop': 'loop', 'en bucle': 'loop', 'цикл': 'loop', '循环': 'loop', 'ループ': 'loop',
1005
- 'affiche': 'poster', 'poster': 'poster', 'cartel': 'poster', 'постер': 'poster', '海报': 'poster', 'ポスター': 'poster',
1006
- 'prechargement': 'preload', 'préchargement': 'preload', 'preload': 'preload', 'precarga': 'preload', 'предзагрузка': 'preload', '预加载': 'preload', 'プリロード': 'preload',
1007
- 'ouvert': 'open', 'open': 'open', 'abierto': 'open', 'открытый': 'open', '打开': 'open', '開く': 'open',
1008
- 'cache': 'hidden', 'caché': 'hidden', 'hidden': 'hidden', 'oculto': 'hidden', 'скрытый': 'hidden', '隐藏': 'hidden', '非表示': 'hidden',
1009
- 'draggable': 'draggable', 'deplacable': 'draggable', 'déplaçable': 'draggable', 'arrastrable': 'draggable', 'перетаскиваемый': 'draggable', '可拖动': 'draggable', 'ドラッグ可能': 'draggable',
1010
- 'editable': 'contenteditable', 'éditable': 'contenteditable', 'contenteditable': 'contenteditable', 'редактируемый': 'contenteditable', '可编辑': 'contenteditable', '編集可能': 'contenteditable',
1011
- 'chargement': 'loading', 'loading': 'loading', 'carga': 'loading', 'загрузка': 'loading', '加载': 'loading', 'ローディング': 'loading',
1012
- 'decodage': 'decoding', 'décodage': 'decoding', 'decoding': 'decoding', 'decodificación': 'decoding', 'декодирование': 'decoding', '解码': 'decoding', 'デコード': 'decoding',
1013
- 'integrite': 'integrity', 'intégrité': 'integrity', 'integrity': 'integrity', 'integridad': 'integrity', 'целостность': 'integrity', '完整性': 'integrity', '整合性': 'integrity',
1014
- 'origine croisee': 'crossorigin', 'origine croisée': 'crossorigin', 'crossorigin': 'crossorigin', 'origen cruzado': 'crossorigin', 'кросс-происхождение': 'crossorigin', '跨域': 'crossorigin', 'クロスオリジン': 'crossorigin',
1015
- 'accepter': 'accept', 'accept': 'accept', 'aceptar': 'accept', 'принимать': 'accept', '接受': 'accept', '受け入れ': 'accept',
1016
- 'taille': 'size', 'size': 'size', 'tamaño': 'size', 'tamano': 'size', 'размер': 'size', '大小': 'size', 'サイズ': 'size',
1017
- 'longueur max': 'maxlength', 'maxlength': 'maxlength', 'longitud maxima': 'maxlength', 'longitud máxima': 'maxlength', 'макс длина': 'maxlength', '最大长度': 'maxlength', '最大長': 'maxlength',
1018
- 'longueur min': 'minlength', 'minlength': 'minlength', 'longitud minima': 'minlength', 'longitud mínima': 'minlength', 'мин длина': 'minlength', '最小长度': 'minlength', '最小長': 'minlength',
1019
- 'formulaire': 'form', 'form': 'form', 'formulario': 'form', 'форма': 'form', '表单': 'form', 'フォーム': 'form',
1020
- 'liste': 'list', 'list': 'list', 'lista': 'list', 'список': 'list', '列表': 'list', 'リスト': 'list',
1021
- 'wrap': 'wrap', 'ajuste': 'wrap', 'перенос': 'wrap', '换行': 'wrap', 'ラップ': 'wrap',
1022
- 'spellcheck': 'spellcheck', 'verification orthographe': 'spellcheck', 'vérification orthographe': 'spellcheck', 'corrector ortografico': 'spellcheck', 'corrector ortográfico': 'spellcheck', 'проверка орфографии': 'spellcheck', '拼写检查': 'spellcheck', 'スペルチェック': 'spellcheck',
1023
- 'inputmode': 'inputmode', 'mode saisie': 'inputmode', 'modo entrada': 'inputmode', 'режим ввода': 'inputmode', '输入模式': 'inputmode', '入力モード': 'inputmode',
1024
- 'enterkeyhint': 'enterkeyhint', 'touche entree': 'enterkeyhint', 'tecla enter': 'enterkeyhint', 'подсказка enter': 'enterkeyhint', 'Enter提示': 'enterkeyhint', 'Enterキーヒント': 'enterkeyhint',
1025
- 'scope': 'scope', 'portee': 'scope', 'portée': 'scope', 'alcance': 'scope', 'область': 'scope', '范围': 'scope', 'スコープ': 'scope',
1026
- 'headers': 'headers', 'entetes': 'headers', 'entêtes': 'headers', 'encabezados': 'headers', 'заголовки': 'headers', '表头': 'headers', 'ヘッダー': 'headers',
1027
- 'defer': 'defer', 'differer': 'defer', 'différer': 'defer', 'diferir': 'defer', 'отложить': 'defer', '延迟': 'defer', '遅延': 'defer',
1028
- 'async': 'async', 'asynchrone': 'async', 'asíncrono': 'async', 'asincrono': 'async', 'асинхронный': 'async', '异步': 'async', '非同期': 'async',
1029
- 'novalidate': 'novalidate', 'sans validation': 'novalidate', 'sin validacion': 'novalidate', 'sin validación': 'novalidate', 'без валидации': 'novalidate', '无验证': 'novalidate', '検証なし': 'novalidate',
1030
- 'enctype': 'enctype', 'encodage type': 'enctype', 'tipo codificacion': 'enctype', 'tipo codificación': 'enctype', 'тип кодировки': 'enctype', '编码类型': 'enctype', 'エンコードタイプ': 'enctype',
1031
- 'download': 'download', 'telecharger': 'download', 'télécharger': 'download', 'descargar': 'download', 'скачать': 'download', '下载': 'download', 'ダウンロード': 'download',
1032
- 'ping': 'ping', 'пинг': 'ping',
1033
- 'referrerpolicy': 'referrerpolicy', 'politique referent': 'referrerpolicy', 'politique référent': 'referrerpolicy', 'politica referente': 'referrerpolicy', 'política referente': 'referrerpolicy', 'политика реферера': 'referrerpolicy', '引用策略': 'referrerpolicy', 'リファラーポリシー': 'referrerpolicy',
1034
- 'sandbox': 'sandbox', 'bac a sable': 'sandbox', 'bac à sable': 'sandbox', 'zona arena': 'sandbox', 'песочница': 'sandbox', '沙盒': 'sandbox', 'サンドボックス': 'sandbox',
1035
- 'allow': 'allow', 'autoriser': 'allow', 'permitir': 'allow', 'разрешить': 'allow', '允许': 'allow', '許可': 'allow',
1036
- 'srcdoc': 'srcdoc', 'document source': 'srcdoc', 'documento fuente': 'srcdoc', 'исходный документ': 'srcdoc', '源文档': 'srcdoc', 'ソースドキュメント': 'srcdoc',
1037
- 'usemap': 'usemap', 'utiliser carte': 'usemap', 'usar mapa': 'usemap', 'использовать карту': 'usemap', '使用地图': 'usemap', 'マップ使用': 'usemap',
1038
- 'ismap': 'ismap', 'est carte': 'ismap', 'es mapa': 'ismap', 'это карта': 'ismap', '是地图': 'ismap', 'マップである': 'ismap',
1039
- 'coords': 'coords', 'coordonnees': 'coords', 'coordonnées': 'coords', 'coordenadas': 'coords', 'координаты': 'coords', '坐标': 'coords', '座標': 'coords',
1040
- 'shape': 'shape', 'forme': 'shape', 'forma': 'shape', 'форма': 'shape', '形状': 'shape', '形': 'shape',
1041
- 'datetime': 'datetime', 'date heure': 'datetime', 'fecha hora': 'datetime', 'дата время': 'datetime', '日期时间': 'datetime', '日時': 'datetime',
1042
- 'cite': 'cite', 'citer': 'cite', 'citar': 'cite', 'цитировать': 'cite', '引用': 'cite',
1043
- 'reversed': 'reversed', 'inverse': 'reversed', 'invertido': 'reversed', 'обратный': 'reversed', '反转': 'reversed', '逆順': 'reversed',
1044
- 'start': 'start', 'debut': 'start', 'début': 'start', 'inicio': 'start', 'начало': 'start', '开始': 'start', '開始': 'start',
1045
- 'high': 'high', 'haut': 'high', 'alto': 'high', 'высокий': 'high', '高': 'high',
1046
- 'low': 'low', 'bas': 'low', 'bajo': 'low', 'низкий': 'low', '低': 'low',
1047
- 'optimum': 'optimum', 'optimal': 'optimum', 'óptimo': 'optimum', 'optimo': 'optimum', 'оптимальный': 'optimum', '最佳': 'optimum', '最適': 'optimum'
1048
- }
1049
-
1050
- return directMap[lowerName] || name
1051
- }
1052
- }
1053
-
1054
- class HTMLCodeGenerator {
1055
- constructor(options = {}) {
1056
- this.indent = 0
1057
- this.indentStr = options.indentStr || ' '
1058
- this.selfClosingStyle = options.selfClosingStyle || 'html'
1059
- }
1060
-
1061
- generate(ast) {
1062
- if (!ast) return ''
1063
-
1064
- switch (ast.type) {
1065
- case 'Document':
1066
- return this.generateDocument(ast)
1067
- case 'Doctype':
1068
- return this.generateDoctype(ast)
1069
- case 'Element':
1070
- return this.generateElement(ast)
1071
- case 'TextNode':
1072
- return this.escapeHtml(ast.content)
1073
- case 'Comment':
1074
- return `<!-- ${ast.content} -->`
1075
- case 'Fragment':
1076
- return ast.children.map(c => this.generate(c)).join('')
1077
- case 'EtherDirective':
1078
- return ''
1079
- case 'EtherLoop':
1080
- return this.generateLoop(ast)
1081
- case 'EtherConditional':
1082
- return this.generateConditional(ast)
1083
- case 'EtherExpression':
1084
- return `<!-- ${ast.expression} -->`
1085
- default:
1086
- return ''
1087
- }
1088
- }
1089
-
1090
- generateDocument(ast) {
1091
- let html = ''
1092
- if (ast.doctype) {
1093
- html += this.generateDoctype(ast.doctype) + '\n'
1094
- }
1095
- if (ast.html) {
1096
- html += this.generate(ast.html)
1097
- }
1098
- return html
1099
- }
1100
-
1101
- generateDoctype(ast) {
1102
- if (ast.publicId) {
1103
- return `<!DOCTYPE ${ast.name} PUBLIC "${ast.publicId}" "${ast.systemId || ''}">`
1104
- }
1105
- return `<!DOCTYPE ${ast.name}>`
1106
- }
1107
-
1108
- generateElement(ast) {
1109
- const tag = ast.htmlTag
1110
- const attrs = this.generateAttributes(ast.attributes)
1111
- const attrStr = attrs ? ' ' + attrs : ''
1112
-
1113
- if (ast.selfClosing || VOID_ELEMENTS.has(tag)) {
1114
- if (this.selfClosingStyle === 'xhtml') {
1115
- return `<${tag}${attrStr} />`
1116
- }
1117
- return `<${tag}${attrStr}>`
1118
- }
1119
-
1120
- const children = ast.children.map(c => this.generate(c)).join('')
1121
-
1122
- if (children.includes('\n') || children.length > 80) {
1123
- const indentedChildren = this.indentContent(children)
1124
- return `<${tag}${attrStr}>\n${indentedChildren}\n${this.getIndent()}</${tag}>`
1125
- }
1126
-
1127
- return `<${tag}${attrStr}>${children}</${tag}>`
1128
- }
1129
-
1130
- generateAttributes(attributes) {
1131
- return attributes.map(attr => {
1132
- if (attr.type === 'BooleanAttribute') {
1133
- return attr.htmlName
1134
- }
1135
- const value = this.escapeAttr(attr.value || '')
1136
- return `${attr.htmlName}="${value}"`
1137
- }).join(' ')
1138
- }
1139
-
1140
- generateLoop(ast) {
1141
- return ast.children.map(c => this.generate(c)).join('')
1142
- }
1143
-
1144
- generateConditional(ast) {
1145
- return ast.thenBranch.map(c => this.generate(c)).join('')
1146
- }
1147
-
1148
- escapeHtml(str) {
1149
- if (!str) return ''
1150
- return str
1151
- .replace(/&/g, '&amp;')
1152
- .replace(/</g, '&lt;')
1153
- .replace(/>/g, '&gt;')
1154
- }
1155
-
1156
- escapeAttr(str) {
1157
- if (!str) return ''
1158
- return str
1159
- .replace(/&/g, '&amp;')
1160
- .replace(/"/g, '&quot;')
1161
- .replace(/</g, '&lt;')
1162
- .replace(/>/g, '&gt;')
1163
- }
1164
-
1165
- getIndent() {
1166
- return this.indentStr.repeat(this.indent)
1167
- }
1168
-
1169
- indentContent(content) {
1170
- this.indent++
1171
- const lines = content.split('\n').map(line => this.getIndent() + line.trim()).join('\n')
1172
- this.indent--
1173
- return lines
1174
- }
1175
- }
1176
-
1177
- module.exports = {
1178
- HTMLLexer,
1179
- HTMLParser,
1180
- HTMLCodeGenerator,
1181
- VOID_ELEMENTS
1182
- }