ether-code 0.1.9 → 0.2.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/cli/ether.js ADDED
@@ -0,0 +1,534 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { EtherCompiler } = require('./compiler')
6
+ const { Watcher } = require('./watcher')
7
+
8
+ const VERSION = '0.2.0'
9
+
10
+ const COLORS = {
11
+ reset: '\x1b[0m',
12
+ bright: '\x1b[1m',
13
+ dim: '\x1b[2m',
14
+ red: '\x1b[31m',
15
+ green: '\x1b[32m',
16
+ yellow: '\x1b[33m',
17
+ blue: '\x1b[34m',
18
+ magenta: '\x1b[35m',
19
+ cyan: '\x1b[36m'
20
+ }
21
+
22
+ function log(msg, color = '') {
23
+ console.log(`${color}${msg}${COLORS.reset}`)
24
+ }
25
+
26
+ function logSuccess(msg) {
27
+ log(`✓ ${msg}`, COLORS.green)
28
+ }
29
+
30
+ function logError(msg) {
31
+ log(`✗ ${msg}`, COLORS.red)
32
+ }
33
+
34
+ function logInfo(msg) {
35
+ log(`ℹ ${msg}`, COLORS.cyan)
36
+ }
37
+
38
+ function logWarning(msg) {
39
+ log(`⚠ ${msg}`, COLORS.yellow)
40
+ }
41
+
42
+ function showBanner() {
43
+ console.log(`
44
+ ${COLORS.cyan}${COLORS.bright}
45
+ ███████╗████████╗██╗ ██╗███████╗██████╗
46
+ ██╔════╝╚══██╔══╝██║ ██║██╔════╝██╔══██╗
47
+ █████╗ ██║ ███████║█████╗ ██████╔╝
48
+ ██╔══╝ ██║ ██╔══██║██╔══╝ ██╔══██╗
49
+ ███████╗ ██║ ██║ ██║███████╗██║ ██║
50
+ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
51
+ ${COLORS.reset}
52
+ ${COLORS.dim}Le langage intentionnel - v${VERSION}${COLORS.reset}
53
+ `)
54
+ }
55
+
56
+ function showHelp() {
57
+ showBanner()
58
+ console.log(`
59
+ ${COLORS.bright}UTILISATION${COLORS.reset}
60
+ ether <commande> [options]
61
+
62
+ ${COLORS.bright}COMMANDES${COLORS.reset}
63
+ ${COLORS.cyan}init${COLORS.reset} Initialiser un nouveau projet Ether
64
+ ${COLORS.cyan}build${COLORS.reset} Compiler les fichiers .eth
65
+ ${COLORS.cyan}dev${COLORS.reset} Mode développement (watch + compilation auto)
66
+ ${COLORS.cyan}check${COLORS.reset} Vérifier la syntaxe sans compiler
67
+ ${COLORS.cyan}help${COLORS.reset} Afficher cette aide
68
+ ${COLORS.cyan}version${COLORS.reset} Afficher la version
69
+
70
+ ${COLORS.bright}OPTIONS${COLORS.reset}
71
+ -c, --config Chemin vers le fichier de configuration
72
+ -o, --output Dossier de sortie
73
+ -w, --watch Surveiller les changements (alias de dev)
74
+ -v, --verbose Mode verbeux
75
+ -q, --quiet Mode silencieux
76
+ --no-color Désactiver les couleurs
77
+
78
+ ${COLORS.bright}EXEMPLES${COLORS.reset}
79
+ ${COLORS.dim}# Initialiser un projet${COLORS.reset}
80
+ ether init
81
+
82
+ ${COLORS.dim}# Compiler le projet${COLORS.reset}
83
+ ether build
84
+
85
+ ${COLORS.dim}# Mode développement${COLORS.reset}
86
+ ether dev
87
+
88
+ ${COLORS.dim}# Compiler avec config personnalisée${COLORS.reset}
89
+ ether build -c ./mon-config.js
90
+
91
+ ${COLORS.bright}DOCUMENTATION${COLORS.reset}
92
+ https://ether-code.com/docs
93
+ `)
94
+ }
95
+
96
+ function showVersion() {
97
+ console.log(`Ether v${VERSION}`)
98
+ }
99
+
100
+ function loadConfig(configPath) {
101
+ const defaultConfig = {
102
+ src: './src',
103
+ out: './dist',
104
+ targets: {},
105
+ i18n: 'fr',
106
+ watch: {
107
+ ignored: ['node_modules', '.git', 'dist']
108
+ }
109
+ }
110
+
111
+ const configFile = configPath || path.join(process.cwd(), 'ether.config.js')
112
+
113
+ if (fs.existsSync(configFile)) {
114
+ try {
115
+ const userConfig = require(configFile)
116
+ return { ...defaultConfig, ...userConfig }
117
+ } catch (err) {
118
+ logError(`Erreur de configuration: ${err.message}`)
119
+ return defaultConfig
120
+ }
121
+ }
122
+
123
+ const jsonConfig = path.join(process.cwd(), 'ether.config.json')
124
+ if (fs.existsSync(jsonConfig)) {
125
+ try {
126
+ const content = fs.readFileSync(jsonConfig, 'utf-8')
127
+ const userConfig = JSON.parse(content)
128
+ return { ...defaultConfig, ...userConfig }
129
+ } catch (err) {
130
+ logError(`Erreur de configuration: ${err.message}`)
131
+ return defaultConfig
132
+ }
133
+ }
134
+
135
+ return defaultConfig
136
+ }
137
+
138
+ async function cmdInit() {
139
+ showBanner()
140
+ logInfo('Initialisation du projet Ether...')
141
+
142
+ const cwd = process.cwd()
143
+
144
+ const srcDir = path.join(cwd, 'src')
145
+ if (!fs.existsSync(srcDir)) {
146
+ fs.mkdirSync(srcDir, { recursive: true })
147
+ logSuccess('Dossier src/ créé')
148
+ }
149
+
150
+ const distDir = path.join(cwd, 'dist')
151
+ if (!fs.existsSync(distDir)) {
152
+ fs.mkdirSync(distDir, { recursive: true })
153
+ logSuccess('Dossier dist/ créé')
154
+ }
155
+
156
+ const configContent = `module.exports = {
157
+ src: './src',
158
+ out: './dist',
159
+
160
+ i18n: 'fr',
161
+
162
+ targets: {
163
+ css: {
164
+ extension: '.css',
165
+ minify: false
166
+ },
167
+ html: {
168
+ extension: '.html',
169
+ minify: false
170
+ },
171
+ js: {
172
+ extension: '.js',
173
+ minify: false
174
+ }
175
+ },
176
+
177
+ watch: {
178
+ ignored: ['node_modules', '.git', 'dist']
179
+ }
180
+ }
181
+ `
182
+
183
+ const configPath = path.join(cwd, 'ether.config.js')
184
+ if (!fs.existsSync(configPath)) {
185
+ fs.writeFileSync(configPath, configContent)
186
+ logSuccess('Fichier ether.config.js créé')
187
+ } else {
188
+ logWarning('ether.config.js existe déjà')
189
+ }
190
+
191
+ const exampleEth = `// cible: html
192
+
193
+ document
194
+ tete
195
+ titre "Ma première page Ether"
196
+
197
+ corps
198
+ entete
199
+ titre1 "Bienvenue sur Ether"
200
+
201
+ principal
202
+ section
203
+ titre2 "Le langage intentionnel"
204
+ paragraphe "Ether permet d'écrire du code comme on pense."
205
+
206
+ pied
207
+ paragraphe "Créé avec Ether"
208
+ `
209
+
210
+ const examplePath = path.join(srcDir, 'index.eth')
211
+ if (!fs.existsSync(examplePath)) {
212
+ fs.writeFileSync(examplePath, exampleEth)
213
+ logSuccess('Fichier exemple src/index.eth créé')
214
+ }
215
+
216
+ console.log('')
217
+ logSuccess('Projet initialisé avec succès!')
218
+ console.log('')
219
+ logInfo('Prochaines étapes:')
220
+ console.log(` 1. Éditer ${COLORS.cyan}src/index.eth${COLORS.reset}`)
221
+ console.log(` 2. Lancer ${COLORS.cyan}ether dev${COLORS.reset} pour le mode développement`)
222
+ console.log(` 3. Ou ${COLORS.cyan}ether build${COLORS.reset} pour compiler`)
223
+ console.log('')
224
+ }
225
+
226
+ async function cmdBuild(options) {
227
+ const startTime = Date.now()
228
+
229
+ if (!options.quiet) {
230
+ showBanner()
231
+ logInfo('Compilation en cours...')
232
+ console.log('')
233
+ }
234
+
235
+ const config = loadConfig(options.config)
236
+ const srcDir = path.resolve(process.cwd(), config.src)
237
+ const outDir = path.resolve(process.cwd(), options.output || config.out)
238
+
239
+ if (!fs.existsSync(srcDir)) {
240
+ logError(`Dossier source introuvable: ${srcDir}`)
241
+ process.exit(1)
242
+ }
243
+
244
+ if (!fs.existsSync(outDir)) {
245
+ fs.mkdirSync(outDir, { recursive: true })
246
+ }
247
+
248
+ const compiler = new EtherCompiler(config)
249
+
250
+ const files = findEthFiles(srcDir)
251
+
252
+ if (files.length === 0) {
253
+ logWarning('Aucun fichier .eth trouvé')
254
+ return
255
+ }
256
+
257
+ let successCount = 0
258
+ let errorCount = 0
259
+
260
+ for (const file of files) {
261
+ try {
262
+ const relativePath = path.relative(srcDir, file)
263
+ const result = await compiler.compileFile(file)
264
+
265
+ for (const output of result.outputs) {
266
+ const outPath = path.join(outDir, output.path)
267
+ const outDirPath = path.dirname(outPath)
268
+
269
+ if (!fs.existsSync(outDirPath)) {
270
+ fs.mkdirSync(outDirPath, { recursive: true })
271
+ }
272
+
273
+ fs.writeFileSync(outPath, output.content)
274
+
275
+ if (options.verbose) {
276
+ logSuccess(`${relativePath} → ${output.path}`)
277
+ }
278
+ }
279
+
280
+ successCount++
281
+ } catch (err) {
282
+ errorCount++
283
+ logError(`${path.relative(srcDir, file)}: ${err.message}`)
284
+
285
+ if (options.verbose) {
286
+ console.error(err.stack)
287
+ }
288
+ }
289
+ }
290
+
291
+ const duration = Date.now() - startTime
292
+
293
+ console.log('')
294
+ if (errorCount === 0) {
295
+ logSuccess(`${successCount} fichier(s) compilé(s) en ${duration}ms`)
296
+ } else {
297
+ logWarning(`${successCount} succès, ${errorCount} erreur(s) en ${duration}ms`)
298
+ }
299
+ }
300
+
301
+ async function cmdDev(options) {
302
+ showBanner()
303
+
304
+ const config = loadConfig(options.config)
305
+ const srcDir = path.resolve(process.cwd(), config.src)
306
+ const outDir = path.resolve(process.cwd(), options.output || config.out)
307
+
308
+ if (!fs.existsSync(srcDir)) {
309
+ logError(`Dossier source introuvable: ${srcDir}`)
310
+ process.exit(1)
311
+ }
312
+
313
+ logInfo(`Surveillance de ${srcDir}`)
314
+ logInfo(`Sortie vers ${outDir}`)
315
+
316
+ await cmdBuild({ ...options, quiet: true })
317
+
318
+ const watcher = new Watcher(srcDir, config.watch)
319
+ const compiler = new EtherCompiler(config)
320
+
321
+ watcher.on('change', async (filePath) => {
322
+ if (!filePath.endsWith('.eth')) return
323
+
324
+ const relativePath = path.relative(srcDir, filePath)
325
+ logInfo(`Modification: ${relativePath}`)
326
+
327
+ try {
328
+ const result = await compiler.compileFile(filePath)
329
+
330
+ for (const output of result.outputs) {
331
+ const outPath = path.join(outDir, output.path)
332
+ const outDirPath = path.dirname(outPath)
333
+
334
+ if (!fs.existsSync(outDirPath)) {
335
+ fs.mkdirSync(outDirPath, { recursive: true })
336
+ }
337
+
338
+ fs.writeFileSync(outPath, output.content)
339
+ logSuccess(`→ ${output.path}`)
340
+ }
341
+ } catch (err) {
342
+ logError(err.message)
343
+ }
344
+ })
345
+
346
+ watcher.on('add', async (filePath) => {
347
+ if (!filePath.endsWith('.eth')) return
348
+
349
+ const relativePath = path.relative(srcDir, filePath)
350
+ logInfo(`Nouveau fichier: ${relativePath}`)
351
+
352
+ try {
353
+ const result = await compiler.compileFile(filePath)
354
+
355
+ for (const output of result.outputs) {
356
+ const outPath = path.join(outDir, output.path)
357
+ const outDirPath = path.dirname(outPath)
358
+
359
+ if (!fs.existsSync(outDirPath)) {
360
+ fs.mkdirSync(outDirPath, { recursive: true })
361
+ }
362
+
363
+ fs.writeFileSync(outPath, output.content)
364
+ logSuccess(`→ ${output.path}`)
365
+ }
366
+ } catch (err) {
367
+ logError(err.message)
368
+ }
369
+ })
370
+
371
+ watcher.on('unlink', (filePath) => {
372
+ if (!filePath.endsWith('.eth')) return
373
+ logWarning(`Fichier supprimé: ${path.relative(srcDir, filePath)}`)
374
+ })
375
+
376
+ watcher.start()
377
+
378
+ process.on('SIGINT', () => {
379
+ console.log('')
380
+ logInfo('Arrêt du mode développement')
381
+ watcher.stop()
382
+ process.exit(0)
383
+ })
384
+ }
385
+
386
+ async function cmdCheck(options) {
387
+ showBanner()
388
+ logInfo('Vérification de la syntaxe...')
389
+ console.log('')
390
+
391
+ const config = loadConfig(options.config)
392
+ const srcDir = path.resolve(process.cwd(), config.src)
393
+
394
+ if (!fs.existsSync(srcDir)) {
395
+ logError(`Dossier source introuvable: ${srcDir}`)
396
+ process.exit(1)
397
+ }
398
+
399
+ const compiler = new EtherCompiler(config)
400
+ const files = findEthFiles(srcDir)
401
+
402
+ if (files.length === 0) {
403
+ logWarning('Aucun fichier .eth trouvé')
404
+ return
405
+ }
406
+
407
+ let validCount = 0
408
+ let errorCount = 0
409
+
410
+ for (const file of files) {
411
+ const relativePath = path.relative(srcDir, file)
412
+
413
+ try {
414
+ await compiler.check(file)
415
+ logSuccess(relativePath)
416
+ validCount++
417
+ } catch (err) {
418
+ logError(`${relativePath}: ${err.message}`)
419
+ errorCount++
420
+ }
421
+ }
422
+
423
+ console.log('')
424
+ if (errorCount === 0) {
425
+ logSuccess(`${validCount} fichier(s) valide(s)`)
426
+ } else {
427
+ logError(`${errorCount} erreur(s) sur ${files.length} fichier(s)`)
428
+ process.exit(1)
429
+ }
430
+ }
431
+
432
+ function findEthFiles(dir) {
433
+ const files = []
434
+
435
+ function scan(currentDir) {
436
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true })
437
+
438
+ for (const entry of entries) {
439
+ const fullPath = path.join(currentDir, entry.name)
440
+
441
+ if (entry.isDirectory()) {
442
+ if (!['node_modules', '.git', 'dist'].includes(entry.name)) {
443
+ scan(fullPath)
444
+ }
445
+ } else if (entry.name.endsWith('.eth')) {
446
+ files.push(fullPath)
447
+ }
448
+ }
449
+ }
450
+
451
+ scan(dir)
452
+ return files
453
+ }
454
+
455
+ function parseArgs(args) {
456
+ const options = {
457
+ command: null,
458
+ config: null,
459
+ output: null,
460
+ verbose: false,
461
+ quiet: false,
462
+ watch: false
463
+ }
464
+
465
+ let i = 0
466
+ while (i < args.length) {
467
+ const arg = args[i]
468
+
469
+ if (arg === '-c' || arg === '--config') {
470
+ options.config = args[++i]
471
+ } else if (arg === '-o' || arg === '--output') {
472
+ options.output = args[++i]
473
+ } else if (arg === '-v' || arg === '--verbose') {
474
+ options.verbose = true
475
+ } else if (arg === '-q' || arg === '--quiet') {
476
+ options.quiet = true
477
+ } else if (arg === '-w' || arg === '--watch') {
478
+ options.watch = true
479
+ } else if (arg === '--no-color') {
480
+ for (const key in COLORS) {
481
+ COLORS[key] = ''
482
+ }
483
+ } else if (!arg.startsWith('-')) {
484
+ if (!options.command) {
485
+ options.command = arg
486
+ }
487
+ }
488
+
489
+ i++
490
+ }
491
+
492
+ return options
493
+ }
494
+
495
+ async function main() {
496
+ const args = process.argv.slice(2)
497
+ const options = parseArgs(args)
498
+
499
+ if (options.watch && !options.command) {
500
+ options.command = 'dev'
501
+ }
502
+
503
+ switch (options.command) {
504
+ case 'init':
505
+ await cmdInit()
506
+ break
507
+ case 'build':
508
+ await cmdBuild(options)
509
+ break
510
+ case 'dev':
511
+ case 'watch':
512
+ await cmdDev(options)
513
+ break
514
+ case 'check':
515
+ await cmdCheck(options)
516
+ break
517
+ case 'version':
518
+ case '-V':
519
+ case '--version':
520
+ showVersion()
521
+ break
522
+ case 'help':
523
+ case '-h':
524
+ case '--help':
525
+ default:
526
+ showHelp()
527
+ break
528
+ }
529
+ }
530
+
531
+ main().catch(err => {
532
+ logError(err.message)
533
+ process.exit(1)
534
+ })
package/cli/watcher.js ADDED
@@ -0,0 +1,106 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const EventEmitter = require('events')
4
+
5
+ class Watcher extends EventEmitter {
6
+ constructor(dir, options = {}) {
7
+ super()
8
+ this.dir = dir
9
+ this.options = {
10
+ ignored: ['node_modules', '.git', 'dist'],
11
+ debounce: 100,
12
+ ...options
13
+ }
14
+ this.watchers = new Map()
15
+ this.debounceTimers = new Map()
16
+ }
17
+
18
+ start() {
19
+ this.watchDir(this.dir)
20
+ }
21
+
22
+ stop() {
23
+ for (const watcher of this.watchers.values()) {
24
+ watcher.close()
25
+ }
26
+ this.watchers.clear()
27
+
28
+ for (const timer of this.debounceTimers.values()) {
29
+ clearTimeout(timer)
30
+ }
31
+ this.debounceTimers.clear()
32
+ }
33
+
34
+ watchDir(dir) {
35
+ if (this.isIgnored(dir)) return
36
+
37
+ try {
38
+ const watcher = fs.watch(dir, { persistent: true }, (eventType, filename) => {
39
+ if (!filename) return
40
+
41
+ const fullPath = path.join(dir, filename)
42
+ this.handleEvent(eventType, fullPath)
43
+ })
44
+
45
+ watcher.on('error', (err) => {
46
+ this.emit('error', err)
47
+ })
48
+
49
+ this.watchers.set(dir, watcher)
50
+
51
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
52
+ for (const entry of entries) {
53
+ if (entry.isDirectory() && !this.isIgnored(entry.name)) {
54
+ this.watchDir(path.join(dir, entry.name))
55
+ }
56
+ }
57
+ } catch (err) {
58
+ this.emit('error', err)
59
+ }
60
+ }
61
+
62
+ handleEvent(eventType, filePath) {
63
+ const existing = this.debounceTimers.get(filePath)
64
+ if (existing) {
65
+ clearTimeout(existing)
66
+ }
67
+
68
+ const timer = setTimeout(() => {
69
+ this.debounceTimers.delete(filePath)
70
+ this.processEvent(filePath)
71
+ }, this.options.debounce)
72
+
73
+ this.debounceTimers.set(filePath, timer)
74
+ }
75
+
76
+ processEvent(filePath) {
77
+ try {
78
+ const exists = fs.existsSync(filePath)
79
+
80
+ if (!exists) {
81
+ this.emit('unlink', filePath)
82
+ return
83
+ }
84
+
85
+ const stat = fs.statSync(filePath)
86
+
87
+ if (stat.isDirectory()) {
88
+ if (!this.watchers.has(filePath)) {
89
+ this.watchDir(filePath)
90
+ this.emit('addDir', filePath)
91
+ }
92
+ } else {
93
+ this.emit('change', filePath)
94
+ }
95
+ } catch (err) {
96
+ this.emit('error', err)
97
+ }
98
+ }
99
+
100
+ isIgnored(name) {
101
+ const baseName = path.basename(name)
102
+ return this.options.ignored.includes(baseName) || baseName.startsWith('.')
103
+ }
104
+ }
105
+
106
+ module.exports = { Watcher }
package/ether-compiler.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
- const { EtherLexer, TokenType } = require('./ether-lexer')
3
+ const { EtherLexer, TokenType } = require('./lexer/ether-lexer')
4
4
  const { EtherParser } = require('./ether-parser')
5
5
  const { CSSGenerator } = require('./generators/css-generator')
6
6
  const { HTMLGenerator } = require('./generators/html-generator')
package/ether-parser.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const fs = require('fs')
2
- const { EtherLexer, Token, TokenType } = require('./ether-lexer')
2
+ const { EtherLexer, Token, TokenType } = require('./lexer/ether-lexer')
3
3
 
4
4
  class EtherParser {
5
5
  constructor(options = {}) {