tusktsk 2.0.1

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.
@@ -0,0 +1,563 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ module TuskLang
7
+ # TuskLang Enhanced Parser for Ruby
8
+ # "We don't bow to any king" - Support ALL syntax styles
9
+ #
10
+ # Features:
11
+ # - Multiple grouping: [], {}, <>
12
+ # - $global vs section-local variables
13
+ # - Cross-file communication
14
+ # - Database queries (placeholder adapters)
15
+ # - All @ operators
16
+ # - Maximum flexibility
17
+ #
18
+ # DEFAULT CONFIG: peanut.tsk (the bridge of language grace)
19
+ class TSKParserEnhanced
20
+ attr_reader :data, :global_variables, :section_variables
21
+
22
+ def initialize
23
+ @data = {}
24
+ @global_variables = {}
25
+ @section_variables = {}
26
+ @cache = {}
27
+ @cross_file_cache = {}
28
+ @current_section = ''
29
+ @in_object = false
30
+ @object_key = ''
31
+ @peanut_loaded = false
32
+
33
+ # Standard peanut.tsk locations
34
+ @peanut_locations = [
35
+ './peanut.tsk',
36
+ '../peanut.tsk',
37
+ '../../peanut.tsk',
38
+ '/etc/tusklang/peanut.tsk',
39
+ File.join(Dir.home, '.config/tusklang/peanut.tsk'),
40
+ ENV['TUSKLANG_CONFIG']
41
+ ].compact
42
+ end
43
+
44
+ # Load peanut.tsk if available
45
+ def load_peanut
46
+ return if @peanut_loaded
47
+
48
+ @peanut_loaded = true # Mark first to prevent recursion
49
+
50
+ @peanut_locations.each do |location|
51
+ next if location.empty?
52
+
53
+ if File.exist?(location)
54
+ puts "# Loading universal config from: #{location}"
55
+ parse_file(location)
56
+ return
57
+ end
58
+ end
59
+ end
60
+
61
+ # Parse TuskLang value with all syntax support
62
+ def parse_value(value)
63
+ value = value.strip
64
+
65
+ # Remove optional semicolon
66
+ value = value.chomp(';').strip if value.end_with?(';')
67
+
68
+ # Basic types
69
+ case value
70
+ when 'true' then return true
71
+ when 'false' then return false
72
+ when 'null' then return nil
73
+ end
74
+
75
+ # Numbers
76
+ return value.to_i if value.match?(/^-?\d+$/)
77
+ return value.to_f if value.match?(/^-?\d+\.\d+$/)
78
+
79
+ # $variable references (global)
80
+ if value.match?(/^\$[a-zA-Z_][a-zA-Z0-9_]*$/)
81
+ var_name = value[1..-1]
82
+ return @global_variables[var_name] || ''
83
+ end
84
+
85
+ # Section-local variable references
86
+ if !@current_section.empty? && value.match?(/^[a-zA-Z_][a-zA-Z0-9_]*$/)
87
+ section_key = "#{@current_section}.#{value}"
88
+ return @section_variables[section_key] if @section_variables.key?(section_key)
89
+ end
90
+
91
+ # @date function
92
+ date_match = value.match(/^@date\(['"](.*)['"]?\)$/)
93
+ if date_match
94
+ format_str = date_match[1]
95
+ return execute_date(format_str)
96
+ end
97
+
98
+ # @env function with default
99
+ env_match = value.match(/^@env\(['"]([^'"]*)['"]?(?:,\s*(.+))?\)$/)
100
+ if env_match
101
+ env_var = env_match[1]
102
+ default_val = env_match[2]&.strip&.tr('"\'', '') || ''
103
+ return ENV[env_var] || default_val
104
+ end
105
+
106
+ # Ranges: 8000-9000
107
+ range_match = value.match(/^(\d+)-(\d+)$/)
108
+ if range_match
109
+ return {
110
+ 'min' => range_match[1].to_i,
111
+ 'max' => range_match[2].to_i,
112
+ 'type' => 'range'
113
+ }
114
+ end
115
+
116
+ # Arrays
117
+ if value.start_with?('[') && value.end_with?(']')
118
+ return parse_array(value)
119
+ end
120
+
121
+ # Objects
122
+ if value.start_with?('{') && value.end_with?('}')
123
+ return parse_object(value)
124
+ end
125
+
126
+ # Cross-file references: @file.tsk.get('key')
127
+ cross_get_match = value.match(/^@([a-zA-Z0-9_-]+)\.tsk\.get\(['"](.*)['"]?\)$/)
128
+ if cross_get_match
129
+ file_name = cross_get_match[1]
130
+ key = cross_get_match[2]
131
+ return cross_file_get(file_name, key)
132
+ end
133
+
134
+ # Cross-file set: @file.tsk.set('key', value)
135
+ cross_set_match = value.match(/^@([a-zA-Z0-9_-]+)\.tsk\.set\(['"]([^'"]*)['"]\?,\s*(.+)\)$/)
136
+ if cross_set_match
137
+ file_name = cross_set_match[1]
138
+ key = cross_set_match[2]
139
+ val = cross_set_match[3]
140
+ return cross_file_set(file_name, key, val)
141
+ end
142
+
143
+ # @query function
144
+ query_match = value.match(/^@query\(['"](.*)['"]?(.*)\)$/)
145
+ if query_match
146
+ query = query_match[1]
147
+ return execute_query(query)
148
+ end
149
+
150
+ # @ operators
151
+ operator_match = value.match(/^@([a-zA-Z_][a-zA-Z0-9_]*)\((.+)\)$/)
152
+ if operator_match
153
+ operator_name = operator_match[1]
154
+ parameters = operator_match[2]
155
+ return execute_operator(operator_name, parameters)
156
+ end
157
+
158
+ # String concatenation
159
+ if value.include?(' + ')
160
+ parts = value.split(' + ')
161
+ result = ''
162
+ parts.each do |part|
163
+ part = part.strip.tr('"\'', '')
164
+ if !part.start_with?('"')
165
+ parsed_part = parse_value(part)
166
+ result += parsed_part.to_s
167
+ else
168
+ result += part[1..-2] if part.length > 1
169
+ end
170
+ end
171
+ return result
172
+ end
173
+
174
+ # Conditional/ternary: condition ? true_val : false_val
175
+ ternary_match = value.match(/(.+?)\s*\?\s*(.+?)\s*:\s*(.+)/)
176
+ if ternary_match
177
+ condition = ternary_match[1].strip
178
+ true_val = ternary_match[2].strip
179
+ false_val = ternary_match[3].strip
180
+
181
+ if evaluate_condition(condition)
182
+ return parse_value(true_val)
183
+ else
184
+ return parse_value(false_val)
185
+ end
186
+ end
187
+
188
+ # Remove quotes from strings
189
+ if (value.start_with?('"') && value.end_with?('"')) ||
190
+ (value.start_with?("'") && value.end_with?("'"))
191
+ return value[1..-2]
192
+ end
193
+
194
+ # Return as-is
195
+ value
196
+ end
197
+
198
+ # Parse array syntax
199
+ def parse_array(value)
200
+ content = value[1..-2].strip
201
+ return [] if content.empty?
202
+
203
+ items = []
204
+ current = ''
205
+ depth = 0
206
+ in_string = false
207
+ quote_char = nil
208
+
209
+ content.each_char do |ch|
210
+ if (ch == '"' || ch == "'") && !in_string
211
+ in_string = true
212
+ quote_char = ch
213
+ elsif ch == quote_char && in_string
214
+ in_string = false
215
+ quote_char = nil
216
+ end
217
+
218
+ unless in_string
219
+ case ch
220
+ when '[', '{'
221
+ depth += 1
222
+ when ']', '}'
223
+ depth -= 1
224
+ when ','
225
+ if depth == 0
226
+ items << parse_value(current.strip)
227
+ current = ''
228
+ next
229
+ end
230
+ end
231
+ end
232
+
233
+ current += ch
234
+ end
235
+
236
+ items << parse_value(current.strip) unless current.strip.empty?
237
+ items
238
+ end
239
+
240
+ # Parse object syntax
241
+ def parse_object(value)
242
+ content = value[1..-2].strip
243
+ return {} if content.empty?
244
+
245
+ pairs = []
246
+ current = ''
247
+ depth = 0
248
+ in_string = false
249
+ quote_char = nil
250
+
251
+ content.each_char do |ch|
252
+ if (ch == '"' || ch == "'") && !in_string
253
+ in_string = true
254
+ quote_char = ch
255
+ elsif ch == quote_char && in_string
256
+ in_string = false
257
+ quote_char = nil
258
+ end
259
+
260
+ unless in_string
261
+ case ch
262
+ when '[', '{'
263
+ depth += 1
264
+ when ']', '}'
265
+ depth -= 1
266
+ when ','
267
+ if depth == 0
268
+ pairs << current.strip
269
+ current = ''
270
+ next
271
+ end
272
+ end
273
+ end
274
+
275
+ current += ch
276
+ end
277
+
278
+ pairs << current.strip unless current.strip.empty?
279
+
280
+ obj = {}
281
+ pairs.each do |pair|
282
+ if pair.include?(':')
283
+ colon_index = pair.index(':')
284
+ key = pair[0...colon_index].strip.tr('"\'', '')
285
+ val = pair[(colon_index + 1)..-1].strip
286
+ obj[key] = parse_value(val)
287
+ elsif pair.include?('=')
288
+ equals_index = pair.index('=')
289
+ key = pair[0...equals_index].strip.tr('"\'', '')
290
+ val = pair[(equals_index + 1)..-1].strip
291
+ obj[key] = parse_value(val)
292
+ end
293
+ end
294
+
295
+ obj
296
+ end
297
+
298
+ # Evaluate conditions for ternary expressions
299
+ def evaluate_condition(condition)
300
+ condition = condition.strip
301
+
302
+ # Simple equality check
303
+ if condition.include?('==')
304
+ left, right = condition.split('==', 2).map(&:strip)
305
+ return parse_value(left).to_s == parse_value(right).to_s
306
+ end
307
+
308
+ # Not equal
309
+ if condition.include?('!=')
310
+ left, right = condition.split('!=', 2).map(&:strip)
311
+ return parse_value(left).to_s != parse_value(right).to_s
312
+ end
313
+
314
+ # Greater than
315
+ if condition.include?('>')
316
+ left, right = condition.split('>', 2).map(&:strip)
317
+ left_val = parse_value(left)
318
+ right_val = parse_value(right)
319
+
320
+ if left_val.is_a?(Numeric) && right_val.is_a?(Numeric)
321
+ return left_val > right_val
322
+ end
323
+ return left_val.to_s > right_val.to_s
324
+ end
325
+
326
+ # Default: check if truthy
327
+ value = parse_value(condition)
328
+ case value
329
+ when true, false
330
+ value
331
+ when String
332
+ !value.empty? && value != 'false' && value != 'null' && value != '0'
333
+ when Numeric
334
+ value != 0
335
+ when nil
336
+ false
337
+ else
338
+ true
339
+ end
340
+ end
341
+
342
+ # Get value from another TSK file
343
+ def cross_file_get(file_name, key)
344
+ cache_key = "#{file_name}:#{key}"
345
+
346
+ # Check cache
347
+ return @cross_file_cache[cache_key] if @cross_file_cache.key?(cache_key)
348
+
349
+ # Find file
350
+ directories = ['.', './config', '..', '../config']
351
+ file_path = nil
352
+
353
+ directories.each do |directory|
354
+ potential_path = File.join(directory, "#{file_name}.tsk")
355
+ if File.exist?(potential_path)
356
+ file_path = potential_path
357
+ break
358
+ end
359
+ end
360
+
361
+ return '' unless file_path
362
+
363
+ # Parse file and get value
364
+ temp_parser = TSKParserEnhanced.new
365
+ temp_parser.parse_file(file_path)
366
+
367
+ value = temp_parser.get(key)
368
+
369
+ # Cache result
370
+ @cross_file_cache[cache_key] = value
371
+
372
+ value
373
+ end
374
+
375
+ # Set value in another TSK file (cache only for now)
376
+ def cross_file_set(file_name, key, value)
377
+ cache_key = "#{file_name}:#{key}"
378
+ parsed_value = parse_value(value)
379
+ @cross_file_cache[cache_key] = parsed_value
380
+ parsed_value
381
+ end
382
+
383
+ # Execute @date function
384
+ def execute_date(format_str)
385
+ now = Time.now
386
+
387
+ # Convert PHP-style format to Ruby
388
+ case format_str
389
+ when 'Y'
390
+ now.strftime('%Y')
391
+ when 'Y-m-d'
392
+ now.strftime('%Y-%m-%d')
393
+ when 'Y-m-d H:i:s'
394
+ now.strftime('%Y-%m-%d %H:%M:%S')
395
+ when 'c'
396
+ now.iso8601
397
+ else
398
+ now.strftime('%Y-%m-%d %H:%M:%S')
399
+ end
400
+ end
401
+
402
+ # Execute database query (placeholder for now)
403
+ def execute_query(query)
404
+ load_peanut
405
+
406
+ # Determine database type
407
+ db_type = get('database.default') || 'sqlite'
408
+
409
+ # Placeholder implementation
410
+ "[Query: #{query} on #{db_type}]"
411
+ end
412
+
413
+ # Execute @ operators
414
+ def execute_operator(operator_name, parameters)
415
+ case operator_name
416
+ when 'cache'
417
+ # Simple cache implementation
418
+ parts = parameters.split(',', 2)
419
+ if parts.length == 2
420
+ ttl = parts[0].strip.tr('"\'', '')
421
+ value = parts[1].strip
422
+ return parse_value(value)
423
+ end
424
+ ''
425
+ when 'learn', 'optimize', 'metrics', 'feature'
426
+ # Placeholders for advanced features
427
+ "@#{operator_name}(#{parameters})"
428
+ else
429
+ "@#{operator_name}(#{parameters})"
430
+ end
431
+ end
432
+
433
+ # Parse a single line
434
+ def parse_line(line)
435
+ trimmed = line.strip
436
+
437
+ # Skip empty lines and comments
438
+ return if trimmed.empty? || trimmed.start_with?('#')
439
+
440
+ # Remove optional semicolon
441
+ trimmed = trimmed.chomp(';').strip if trimmed.end_with?(';')
442
+
443
+ # Check for section declaration []
444
+ section_match = trimmed.match(/^\[([a-zA-Z_][a-zA-Z0-9_]*)\]$/)
445
+ if section_match
446
+ @current_section = section_match[1]
447
+ @in_object = false
448
+ return
449
+ end
450
+
451
+ # Check for angle bracket object >
452
+ angle_open_match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*>$/)
453
+ if angle_open_match
454
+ @in_object = true
455
+ @object_key = angle_open_match[1]
456
+ return
457
+ end
458
+
459
+ # Check for closing angle bracket <
460
+ if trimmed == '<'
461
+ @in_object = false
462
+ @object_key = ''
463
+ return
464
+ end
465
+
466
+ # Check for curly brace object {
467
+ brace_open_match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*\{$/)
468
+ if brace_open_match
469
+ @in_object = true
470
+ @object_key = brace_open_match[1]
471
+ return
472
+ end
473
+
474
+ # Check for closing curly brace }
475
+ if trimmed == '}'
476
+ @in_object = false
477
+ @object_key = ''
478
+ return
479
+ end
480
+
481
+ # Parse key-value pairs (both : and = supported)
482
+ kv_match = trimmed.match(/^([\$]?[a-zA-Z_][a-zA-Z0-9_-]*)\s*[:=]\s*(.+)$/)
483
+ if kv_match
484
+ key = kv_match[1]
485
+ value = kv_match[2]
486
+ parsed_value = parse_value(value)
487
+
488
+ # Determine storage location
489
+ storage_key = if @in_object && !@object_key.empty?
490
+ if !@current_section.empty?
491
+ "#{@current_section}.#{@object_key}.#{key}"
492
+ else
493
+ "#{@object_key}.#{key}"
494
+ end
495
+ elsif !@current_section.empty?
496
+ "#{@current_section}.#{key}"
497
+ else
498
+ key
499
+ end
500
+
501
+ # Store the value
502
+ @data[storage_key] = parsed_value
503
+
504
+ # Handle global variables
505
+ if key.start_with?('$')
506
+ var_name = key[1..-1]
507
+ @global_variables[var_name] = parsed_value
508
+ elsif !@current_section.empty? && !key.start_with?('$')
509
+ # Store section-local variable
510
+ section_key = "#{@current_section}.#{key}"
511
+ @section_variables[section_key] = parsed_value
512
+ end
513
+ end
514
+ end
515
+
516
+ # Parse TuskLang content
517
+ def parse(content)
518
+ lines = content.split("\n")
519
+
520
+ lines.each { |line| parse_line(line) }
521
+
522
+ @data
523
+ end
524
+
525
+ # Parse a TSK file
526
+ def parse_file(file_path)
527
+ content = File.read(file_path)
528
+ parse(content)
529
+ end
530
+
531
+ # Get a value by key
532
+ def get(key)
533
+ @data[key]
534
+ end
535
+
536
+ # Set a value
537
+ def set(key, value)
538
+ @data[key] = value
539
+ end
540
+
541
+ # Get all keys
542
+ def keys
543
+ @data.keys.sort
544
+ end
545
+
546
+ # Get all key-value pairs
547
+ def items
548
+ @data.dup
549
+ end
550
+
551
+ # Convert to JSON
552
+ def to_json
553
+ JSON.pretty_generate(@data)
554
+ end
555
+
556
+ # Load configuration from peanut.tsk
557
+ def self.load_from_peanut
558
+ parser = new
559
+ parser.load_peanut
560
+ parser
561
+ end
562
+ end
563
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TuskLang
4
+ VERSION = "2.0.1"
5
+ end
data/lib/tusk_lang.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TuskLang Ruby SDK
4
+ # The official TuskLang Configuration SDK for Ruby
5
+
6
+ require_relative "tusk_lang/version"
7
+ require_relative "tusk_lang/tsk"
8
+ require_relative "tusk_lang/tsk_parser"
9
+ require_relative "tusk_lang/shell_storage"
10
+
11
+ module TuskLang
12
+ class Error < StandardError; end
13
+ # Your code goes here...
14
+ end