livetext 0.9.55 → 0.9.58

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/livetext/ast/show_ast_clean.rb +10 -0
  3. data/lib/livetext/ast/show_ast_result.rb +60 -0
  4. data/lib/livetext/ast/show_raw_arrays.rb +13 -0
  5. data/lib/livetext/ast.rb +464 -0
  6. data/lib/livetext/ast_to_html.rb +32 -0
  7. data/lib/livetext/core.rb +6 -4
  8. data/lib/livetext/errors.rb +1 -0
  9. data/lib/livetext/functions.rb +26 -0
  10. data/lib/livetext/handler/mixin.rb +28 -0
  11. data/lib/livetext/helpers.rb +19 -20
  12. data/lib/livetext/standard.rb +2 -2
  13. data/lib/livetext/userapi.rb +20 -1
  14. data/lib/livetext/variable_manager.rb +14 -1
  15. data/lib/livetext/variables.rb +5 -1
  16. data/lib/livetext/version.rb +1 -1
  17. data/plugin/booktool.rb +6 -6
  18. data/plugin/lt3scriptor.rb +914 -0
  19. data/plugin/mixin_functions_class.rb +33 -0
  20. data/test/snapshots/error_missing_end/match-error.txt +1 -1
  21. data/test/snapshots/mixin_functions_class/expected-error.txt +0 -0
  22. data/test/snapshots/mixin_functions_class/expected-output.txt +20 -0
  23. data/test/snapshots/mixin_functions_class/match-error.txt +1 -0
  24. data/test/snapshots/mixin_functions_class/mixin_functions_class.rb +33 -0
  25. data/test/snapshots/mixin_functions_class/source.lt3 +17 -0
  26. data/test/snapshots/system_info/match-output.txt +18 -0
  27. data/test/unit/all.rb +3 -0
  28. data/test/unit/ast.rb +90 -0
  29. data/test/unit/ast_directives.rb +104 -0
  30. data/test/unit/ast_variables.rb +71 -0
  31. data/test/unit/core_methods.rb +180 -0
  32. data/test/unit/formatter_component.rb +1 -1
  33. data/test/unit/mixin_functions_class.rb +131 -0
  34. data/test/unit/stringparser.rb +14 -32
  35. metadata +19 -5
  36. data/imports/markdown.rb +0 -44
  37. data/plugin/markdown.rb +0 -43
  38. data/test/snapshots/system_info/expected-output.txt +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00c147a97637c967777f1a1a8bae52d8e0ca8e65f3e032e9a03f0f3bbc2c5d50
4
- data.tar.gz: c931920c669cf05c76176d19bcfb9c06149d3bf6b32167aa2e6da7b50290adef
3
+ metadata.gz: 03b835ce30fd58d0d0bdca2902661242763e9ea1c9cc71bb49bad16543b9f2f0
4
+ data.tar.gz: 36e4bf16acf045e4960ba5e5238c33561f640c63ae80be628805b15ccdefc00d
5
5
  SHA512:
6
- metadata.gz: 59836de9481a1239cf0c0f00f55681da634c7e96e337c67d2c4fb8974ec3dad06fa83bbe7eb7f73596c73d97e9359aab613efbf87d0ddfe70e9a3a50c65e931e
7
- data.tar.gz: b0e1a74d2d75e8c47afff0881594d1022dbb63e71ec67b8304622b9885bc62429d6782286482a45520587207fc48e4158d0a857f51bcac5596c51cc779f2cf01
6
+ metadata.gz: ba6cdce7c715a47048bd48cb66428afcd8510466df09d7c946b1c1aa156aa7216bb3695a5cbdb0b5d0a9954f6d6a3cd17eb79d8c0d05ab6e143eb542e5b66eed
7
+ data.tar.gz: 20a0995a45aba208d232265dd4f2fcff0f2d28bff1deb5b05b743b5c8fa01f7b3ea2ba32b22c9426eba1421ed665b0c1606561b268f1b109df69addc7e084f97
@@ -0,0 +1,10 @@
1
+ require_relative 'lib/livetext/ast'
2
+
3
+ ast = LivetextAST.new
4
+ lines = File.readlines('examples/example1/README.lt3').map(&:chomp)
5
+ result = ast.parse_directives(lines)
6
+
7
+ puts "AST RESULT:"
8
+ puts "=" * 50
9
+ puts LivetextAST.inspect_ast(result)
10
+ puts "=" * 50
@@ -0,0 +1,60 @@
1
+ require_relative 'lib/livetext/ast'
2
+
3
+ ast = LivetextAST.new
4
+
5
+ puts "=== Complete AST for README.lt3 ==="
6
+ puts
7
+
8
+ # Read the file
9
+ lines = File.readlines('examples/example1/README.lt3').map(&:chomp)
10
+
11
+ # Parse directives
12
+ directive_result = ast.parse_directives(lines)
13
+
14
+ puts "File has #{lines.length} lines"
15
+ puts "Found #{directive_result.is_a?(Array) ? directive_result.length : 1} directives"
16
+ puts
17
+
18
+ puts "=== AST Structure (Pretty Printed) ==="
19
+ puts LivetextAST.inspect_ast(directive_result)
20
+ puts
21
+
22
+ puts "=== Summary by Directive Type ==="
23
+ if directive_result.is_a?(Array)
24
+ directive_types = directive_result.map { |d| d[1] if d.is_a?(Array) && d[0] == :directive }.compact
25
+ type_counts = directive_types.group_by(&:itself).transform_values(&:count)
26
+ type_counts.sort.each do |type, count|
27
+ puts "#{type}: #{count}"
28
+ end
29
+ else
30
+ puts "Single directive: #{directive_result[1] if directive_result.is_a?(Array)}"
31
+ end
32
+ puts
33
+
34
+ puts "=== Sample Text Lines with Formatting ==="
35
+ text_lines = lines.reject { |line| line.start_with?('.') || line.strip.empty? }
36
+ sample_lines = text_lines.first(10)
37
+
38
+ sample_lines.each_with_index do |line, i|
39
+ puts "Line #{i+1}: #{line.inspect}"
40
+
41
+ # Parse variables
42
+ var_result = ast.parse_variables(line)
43
+ if var_result != line
44
+ puts " Variables: #{LivetextAST.inspect_ast(var_result)}"
45
+ end
46
+
47
+ # Parse functions
48
+ func_result = ast.parse_functions(line)
49
+ if func_result != line
50
+ puts " Functions: #{LivetextAST.inspect_ast(func_result)}"
51
+ end
52
+
53
+ # Parse formatting
54
+ format_result = ast.parse_inline_formatting(line)
55
+ if format_result != line
56
+ puts " Formatting: #{LivetextAST.inspect_ast(format_result)}"
57
+ end
58
+
59
+ puts
60
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'lib/livetext/ast'
2
+
3
+ ast = LivetextAST.new
4
+ lines = File.readlines('examples/example1/README.lt3').map(&:chomp)
5
+ result = ast.parse_directives(lines)
6
+
7
+ puts "RAW ARRAYS:"
8
+ puts "=" * 50
9
+ result.each_with_index do |directive, i|
10
+ puts "Directive #{i+1}: #{directive.inspect}"
11
+ puts
12
+ end
13
+ puts "=" * 50
@@ -0,0 +1,464 @@
1
+ # Livetext AST Parser for Inline Formatting, Variables, and Functions
2
+ # Parses Livetext syntax into s-expression style Ruby arrays
3
+ #
4
+ # Features:
5
+ # - Inline formatting: *bold, _italic, `code, ~strike, **double, *[bracketed]
6
+ # - Variables: $name, $my_var, $font.title (with validation)
7
+ # - Functions: $$func, $$func:param, $$func[param], $$func (space/eol)
8
+ # - Escaped characters: \*, \_, \`, \~, \$, \$\$
9
+ # - Parameter delimiter tracking for functions (:space, :eol, :colon, :lbrack)
10
+ #
11
+ # Edge cases handled to match Livetext behavior:
12
+ # - $foo. -> parses $foo as variable, . as literal
13
+ # - $a..b -> parses $a as variable, ..b as literal
14
+ # - Invalid names left as literal text
15
+
16
+ class LivetextAST
17
+ # AST node type constants
18
+ TEXT = :text
19
+ BOLD = :bold
20
+ ITALIC = :italic
21
+ CODE = :code
22
+ STRIKE = :strike
23
+ VAR = :var
24
+ FUNC = :func
25
+ BODY = :body
26
+ DIRECTIVE = :directive
27
+ ERROR = :error
28
+
29
+ # Function parameter delimiter constants
30
+ SPACE = :space
31
+ EOL = :eol
32
+ COLON = :colon
33
+ LBRACK = :lbrack
34
+ def initialize
35
+ @escaped_chars = {}
36
+ @escape_counter = 0
37
+ end
38
+
39
+ def parse_inline_formatting(text)
40
+ return [] if text.nil? || text.empty?
41
+
42
+ # Step 1: Mark escaped characters
43
+ text = mark_escaped_characters(text)
44
+
45
+ # Step 2: Parse in order of precedence
46
+ result = parse_bracketed_markers(text)
47
+ result = parse_double_markers(result)
48
+ result = parse_single_markers(result)
49
+
50
+ # Step 3: Restore escaped characters
51
+ result = restore_escaped_characters(result)
52
+
53
+ # Step 4: Convert to s-expression format
54
+ convert_to_formatting_sexpr(result)
55
+ end
56
+
57
+ def parse_variables(text)
58
+ return [] if text.nil? || text.empty?
59
+
60
+ # Step 1: Mark escaped dollar signs
61
+ text = mark_escaped_dollars(text)
62
+
63
+ # Step 2: Parse variables
64
+ result = parse_variable_markers(text)
65
+
66
+ # Step 3: Restore escaped characters
67
+ result = restore_escaped_characters(result)
68
+
69
+ # Step 4: Convert to s-expression format
70
+ convert_to_variable_sexpr(result)
71
+ end
72
+
73
+ def parse_functions(text)
74
+ return [] if text.nil? || text.empty?
75
+
76
+ # Step 1: Mark escaped dollar signs
77
+ text = mark_escaped_dollars(text)
78
+
79
+ # Step 2: Parse functions
80
+ result = parse_function_markers(text)
81
+
82
+ # Step 3: Restore escaped characters
83
+ result = restore_escaped_characters(result)
84
+
85
+ # Step 4: Convert to s-expression format
86
+ convert_to_function_sexpr(result)
87
+ end
88
+
89
+ def parse_directives(lines)
90
+ return [] if lines.nil? || lines.empty?
91
+
92
+ # Step 2: Parse each directive
93
+ result = []
94
+ i = 0
95
+ while i < lines.length
96
+ line = lines[i]
97
+ if line.start_with?('.') && line.strip != ".end"
98
+ directive_result = parse_single_directive(lines, i)
99
+ if directive_result.is_a?(Array) && directive_result.first == ERROR
100
+ # Error occurred, return the error
101
+ return directive_result
102
+ else
103
+ result << directive_result
104
+ # Skip to the line after the directive (including body if any)
105
+ if directive_result.is_a?(Array) && directive_result.first == DIRECTIVE && directive_result.length == 4
106
+ # Multi-line directive with body, find the .end
107
+ j = i + 1
108
+ while j < lines.length && lines[j].strip != ".end"
109
+ j += 1
110
+ end
111
+ i = j # Skip to after .end
112
+ end
113
+ end
114
+ end
115
+ i += 1
116
+ end
117
+
118
+ # Step 3: Return single directive or array of directives
119
+ result.length == 1 ? result.first : result
120
+ end
121
+
122
+ private
123
+
124
+ def mark_escaped_characters(str)
125
+ str.gsub(/\\([*_`~])/) do |match|
126
+ marker = "ESCAPED_#{@escape_counter}"
127
+ @escaped_chars[marker] = $1
128
+ @escape_counter += 1
129
+ marker
130
+ end
131
+ end
132
+
133
+ def mark_escaped_dollars(str)
134
+ str.gsub(/\\(\$+)/) do |match|
135
+ marker = "ESCAPED_#{@escape_counter}"
136
+ @escaped_chars[marker] = match # Store the full escaped sequence
137
+ @escape_counter += 1
138
+ marker
139
+ end
140
+ end
141
+
142
+ def restore_escaped_characters(str)
143
+ @escaped_chars.each do |marker, char|
144
+ str = str.gsub(marker, char)
145
+ end
146
+ @escaped_chars.clear # Reset for next use
147
+ str
148
+ end
149
+
150
+ def parse_bracketed_markers(text)
151
+ # Handle *[content], _[content], `[content], ~[content]
152
+ text.gsub(/([*_`~])\[([^\]]*)\]/) do |match|
153
+ marker, content = $1, $2
154
+ if content.empty?
155
+ "" # empty brackets disappear
156
+ else
157
+ format_type = case marker
158
+ when "*" then BOLD
159
+ when "_" then ITALIC
160
+ when "`" then CODE
161
+ when "~" then STRIKE
162
+ end
163
+ "FORMAT_#{format_type}_#{content}_END"
164
+ end
165
+ end
166
+ end
167
+
168
+ def parse_double_markers(text)
169
+ # Handle **word, text and **word. text
170
+ text.gsub(/(?<=\s|^)\*\*([^\s,.]*)/) do |match|
171
+ if $1.empty?
172
+ "**" # standalone ** should be literal
173
+ else
174
+ "FORMAT_bold_#{$1}_END"
175
+ end
176
+ end
177
+ end
178
+
179
+ def parse_single_markers(text)
180
+ # Handle *word, _word, `word, ~word
181
+ text.gsub(/(?<=\s|^)([*_`~])(?!\[)(?!\*)([^\s]*)/) do |match|
182
+ marker, content = $1, $2
183
+ if content.empty?
184
+ marker # standalone marker should be literal
185
+ else
186
+ format_type = case marker
187
+ when "*" then BOLD
188
+ when "_" then ITALIC
189
+ when "`" then CODE
190
+ when "~" then STRIKE
191
+ end
192
+ "FORMAT_#{format_type}_#{content}_END"
193
+ end
194
+ end
195
+ end
196
+
197
+ def parse_variable_markers(text)
198
+ # Handle $variable names
199
+ # Valid names: start with letter, contain letters/numbers/underscores, can have periods as separators
200
+ # Must be followed by word boundary or end of string
201
+ text.gsub(/\$([a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*)(?=\b|$)/) do |match|
202
+ var_name = $1
203
+ if valid_variable_name?(var_name)
204
+ "VAR_#{var_name}_END"
205
+ else
206
+ match # leave as literal if invalid
207
+ end
208
+ end
209
+ end
210
+
211
+ def parse_function_markers(text)
212
+ # Handle $$function names with parameters
213
+ # Pattern: $$name, $$name:param, $$name[param], $$name (space or eol)
214
+ text.gsub(/\$\$([a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*)(?:\s|$|:([^\s]+)|\[([^\]]*)\])?/) do |match|
215
+ func_name = $1
216
+ colon_param = $3
217
+ bracket_param = $4
218
+
219
+ if valid_function_name?(func_name)
220
+ if colon_param
221
+ "FUNC_#{func_name}_COLON_#{colon_param}_END"
222
+ elsif bracket_param
223
+ "FUNC_#{func_name}_LBRACK_#{bracket_param}_END"
224
+ elsif match.end_with?(" ")
225
+ "FUNC_#{func_name}_SPACE_END"
226
+ else
227
+ "FUNC_#{func_name}_EOL_END"
228
+ end
229
+ else
230
+ match # leave as literal if invalid
231
+ end
232
+ end
233
+ end
234
+
235
+ def valid_variable_name?(name)
236
+ # Must start with letter, contain only letters/numbers/underscores/periods
237
+ # No consecutive periods, no trailing period, no leading period
238
+ return false unless name =~ /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*$/
239
+ return false if name.include?("..") || name.end_with?(".") || name.start_with?(".")
240
+ true
241
+ end
242
+
243
+ def valid_function_name?(name)
244
+ # Same rules as variables
245
+ valid_variable_name?(name)
246
+ end
247
+
248
+ def parse_single_directive(lines, start_line)
249
+ line = lines[start_line]
250
+ line_num = start_line + 1
251
+
252
+ # Handle dot-space comments (single line)
253
+ if line =~ /^\.\s+(.*)$/
254
+ return [DIRECTIVE, "comment", $1]
255
+ end
256
+
257
+ # Parse directive name and arguments
258
+ if line =~ /^\.(\w+)(?:\s+(.*))?$/
259
+ directive_name = $1
260
+ args = $2 || ""
261
+
262
+ # Check if directive exists in standard.rb
263
+ unless directive_exists?(directive_name)
264
+ return [ERROR, "Unknown directive", line_num, directive_name]
265
+ end
266
+
267
+ # Check if directive takes a body (3 parameters)
268
+ if directive_has_body?(directive_name)
269
+ # Parse body until .end
270
+ body_lines = []
271
+ i = start_line + 1
272
+
273
+ while i < lines.length
274
+ current_line = lines[i]
275
+ if current_line.strip == ".end"
276
+ return [DIRECTIVE, directive_name, args, [BODY, body_lines]]
277
+ elsif current_line.strip.start_with?('.') && current_line.strip != ".end"
278
+ # Found another directive inside body - for rough approximation, just include it
279
+ # In real Livetext this would be an error, but we're being permissive
280
+ body_lines << current_line
281
+ else
282
+ body_lines << current_line
283
+ end
284
+ i += 1
285
+ end
286
+
287
+ # Reached end of file without .end
288
+ return [ERROR, "Missing .end", line_num, directive_name]
289
+ else
290
+ # Single line directive
291
+ return [DIRECTIVE, directive_name, args]
292
+ end
293
+ else
294
+ return [ERROR, "Invalid directive syntax", line_num, line.strip]
295
+ end
296
+ end
297
+
298
+ def directive_exists?(name)
299
+ # .end is a special case - it's not a directive but a terminator
300
+ return false if name == "end"
301
+
302
+ # For now, assume any word after a dot is a valid directive
303
+ # This is a rough approximation that will catch most cases
304
+ true
305
+ end
306
+
307
+ def directive_has_body?(name)
308
+ # Heuristic: if we don't know about a directive, assume it might have a body
309
+ # and let the parsing logic figure it out by looking for .end
310
+
311
+ # Known single-line directives (definitely no body)
312
+ single_line_directives = %w[
313
+ h1 h2 h3 h4 h5 h6 set variables variables! errout ttyout say banner
314
+ cleanup mono br reflection backtrace passthru nopass para nopara
315
+ heading newpage cinclude dot_include inherit mixin import copy r
316
+ raw debug seek title section testcase
317
+ ]
318
+
319
+ # If it's in the single-line list, it definitely doesn't have a body
320
+ return false if single_line_directives.include?(name)
321
+
322
+ # Otherwise, assume it might have a body and let the parser check for .end
323
+ true
324
+ end
325
+
326
+ # Custom inspect method for prettier AST output
327
+ def self.inspect_ast(ast)
328
+ case ast
329
+ when Array
330
+ if ast.empty?
331
+ "[]"
332
+ elsif ast.first.is_a?(Symbol)
333
+ case ast.first
334
+ when TEXT
335
+ "[TEXT, #{ast[1..-1].map { |part| inspect_ast(part) }.join(', ')}]"
336
+ when BOLD
337
+ "[BOLD, #{ast[1].inspect}]"
338
+ when ITALIC
339
+ "[ITALIC, #{ast[1].inspect}]"
340
+ when CODE
341
+ "[CODE, #{ast[1].inspect}]"
342
+ when STRIKE
343
+ "[STRIKE, #{ast[1].inspect}]"
344
+ when VAR
345
+ "[VAR, #{ast[1].inspect}]"
346
+ when FUNC
347
+ delimiter = ast[2]
348
+ param = ast[3]
349
+ if param
350
+ "[FUNC, #{ast[1].inspect}, #{delimiter}, #{param.inspect}]"
351
+ else
352
+ "[FUNC, #{ast[1].inspect}, #{delimiter}]"
353
+ end
354
+ when BODY
355
+ "[BODY, #{ast[1].inspect}]"
356
+ when DIRECTIVE
357
+ if ast.length == 3
358
+ "[DIRECTIVE, #{ast[1].inspect}, #{ast[2].inspect}]"
359
+ else
360
+ "[DIRECTIVE, #{ast[1].inspect}, #{ast[2].inspect}, #{inspect_ast(ast[3])}]"
361
+ end
362
+ when ERROR
363
+ "[ERROR, #{ast[1].inspect}, #{ast[2]}, #{ast[3].inspect}]"
364
+ else
365
+ ast.inspect
366
+ end
367
+ else
368
+ ast.inspect
369
+ end
370
+ when String
371
+ ast.inspect
372
+ else
373
+ ast.inspect
374
+ end
375
+ end
376
+
377
+ def convert_to_formatting_sexpr(text)
378
+ # Split by FORMAT markers and convert to s-expressions
379
+ parts = text.split(/(FORMAT_\w+_[^_]*_END)/)
380
+
381
+ result = []
382
+ parts.each do |part|
383
+ if part.start_with?("FORMAT_")
384
+ # Parse format marker
385
+ if part =~ /FORMAT_(\w+)_([^_]*)_END/
386
+ format_type = $1.to_sym
387
+ content = $2
388
+ result << [format_type, content]
389
+ end
390
+ else
391
+ # Plain text
392
+ result << part unless part.empty?
393
+ end
394
+ end
395
+
396
+ # Handle edge cases
397
+ if result.empty?
398
+ return []
399
+ elsif result.length == 1
400
+ return result.first
401
+ else
402
+ return [TEXT, *result]
403
+ end
404
+ end
405
+
406
+ def convert_to_variable_sexpr(text)
407
+ # Split by VAR markers and convert to s-expressions
408
+ parts = text.split(/(VAR_.*?_END)/)
409
+
410
+ result = []
411
+ parts.each do |part|
412
+ if part.start_with?("VAR_")
413
+ # Parse variable marker
414
+ if part =~ /VAR_(.*?)_END/
415
+ var_name = $1
416
+ result << [VAR, var_name]
417
+ end
418
+ else
419
+ # Plain text
420
+ result << part unless part.empty?
421
+ end
422
+ end
423
+
424
+ # Handle edge cases
425
+ if result.empty?
426
+ return []
427
+ elsif result.length == 1
428
+ return result.first
429
+ else
430
+ return [:text, *result]
431
+ end
432
+ end
433
+
434
+ def convert_to_function_sexpr(text)
435
+ # Split by FUNC markers and convert to s-expressions
436
+ parts = text.split(/(FUNC_.*?_(?:SPACE|EOL|COLON|LBRACK)(?:_.*?)?_END)/)
437
+
438
+ result = []
439
+ parts.each do |part|
440
+ if part.start_with?("FUNC_")
441
+ # Parse function marker
442
+ if part =~ /FUNC_(.*?)_(SPACE|EOL|COLON|LBRACK)(?:_(.*?))?_END/
443
+ func_name = $1
444
+ delimiter = $2.downcase.to_sym
445
+ param = $3
446
+ param = nil if param.nil? || param.empty? || delimiter == SPACE || delimiter == EOL
447
+ result << [FUNC, func_name, delimiter, param]
448
+ end
449
+ else
450
+ # Plain text
451
+ result << part unless part.empty?
452
+ end
453
+ end
454
+
455
+ # Handle edge cases
456
+ if result.empty?
457
+ return []
458
+ elsif result.length == 1
459
+ return result.first
460
+ else
461
+ return [TEXT, *result]
462
+ end
463
+ end
464
+ end
@@ -0,0 +1,32 @@
1
+ # AST to HTML Converter
2
+ # Converts LivetextAST output back to HTML
3
+
4
+ class LivetextASTToHTML
5
+ def self.convert(ast)
6
+ case ast
7
+ when Array
8
+ if ast.empty?
9
+ "" # empty array maps to empty string
10
+ elsif ast.first == :text
11
+ # [:text, "Hello ", [:bold, "world"], "!"]
12
+ ast[1..-1].map { |part| convert(part) }.join
13
+ elsif ast.length == 2
14
+ # [:bold, "content"], [:italic, "content"], etc.
15
+ format_type, content = ast
16
+ case format_type
17
+ when :bold then "<b>#{content}</b>"
18
+ when :italic then "<i>#{content}</i>"
19
+ when :code then "<tt>#{content}</tt>"
20
+ when :strike then "<strike>#{content}</strike>"
21
+ else content # fallback
22
+ end
23
+ else
24
+ ast.to_s # fallback
25
+ end
26
+ when String
27
+ ast
28
+ else
29
+ ast.to_s
30
+ end
31
+ end
32
+ end
data/lib/livetext/core.rb CHANGED
@@ -78,8 +78,10 @@ class Livetext
78
78
  mix.each do |lib|
79
79
  obj.invoke_dotcmd(:mixin, lib.dup)
80
80
  end
81
- call.each {|cmd| obj.send(cmd[1..-1]) } # ignores leading dot, no param
81
+ call.each {|cmd| obj.handle_dotcmd(cmd) } # Use handle_dotcmd for proper command parsing
82
82
  obj.api.setvars(vars)
83
+ # Also set variables in global Livetext::Vars for backward compatibility
84
+ vars.each {|var, val| Vars[var.to_sym] = val.to_s }
83
85
  obj
84
86
  end
85
87
 
@@ -87,9 +89,11 @@ class Livetext
87
89
  mix = Array(mix)
88
90
  call = Array(call)
89
91
  mix.each {|lib| mixin(lib) }
90
- call.each {|cmd| send(cmd[1..-1]) } # ignores leading dot, no param
92
+ call.each {|cmd| handle_dotcmd(cmd) } # Use handle_dotcmd for proper command parsing
91
93
  # vars.each_pair {|var, val| @api.set(var, val.to_s) }
92
94
  api.setvars(vars)
95
+ # Also set variables in global Livetext::Vars for backward compatibility
96
+ vars.each {|var, val| Vars[var.to_sym] = val.to_s }
93
97
  self
94
98
  end
95
99
 
@@ -154,8 +158,6 @@ class Livetext
154
158
  @api = obj
155
159
  end
156
160
 
157
-
158
-
159
161
  def process(text: nil, file: nil, vars: {})
160
162
  # Set variables first
161
163
  @variables.set_multiple(vars) unless vars.empty?
@@ -67,6 +67,7 @@ end
67
67
  make_exception(:EndWithoutOpening, "Error: found .end with no opening command")
68
68
  make_exception(:UnknownMethod, "Error: name '%1' is unknown")
69
69
  make_exception(:NoSuchFile, "Error: can't find file '%1' (method '%2')")
70
+ make_exception(:ExpectedEnd, "Error: expected .end but found end of file")
70
71
 
71
72
  # Move others here? DisallowedName, etc.
72
73
 
@@ -11,6 +11,32 @@ class Livetext::Functions
11
11
  attr_accessor :param # kill this?
12
12
  end
13
13
 
14
+ # Instance variables for accessing the Livetext instance and its variables
15
+ attr_accessor :live, :vars
16
+
17
+ def initialize
18
+ @live = nil
19
+ @vars = nil
20
+ end
21
+
22
+ # Helper method to access variables with fallback to global Livetext::Vars
23
+ def get_var(name)
24
+ return @vars.get(name) if @vars
25
+ return @live&.vars.get(name) if @live&.vars
26
+ Livetext::Vars[name]
27
+ end
28
+
29
+ # Helper method to set variables
30
+ def set_var(name, value)
31
+ if @vars
32
+ @vars.set(name, value)
33
+ elsif @live&.vars
34
+ @live.vars.set(name, value)
35
+ else
36
+ Livetext::Vars[name] = value
37
+ end
38
+ end
39
+
14
40
  def code_lines(param = nil)
15
41
  $code_lines.to_i # FIXME pleeease
16
42
  end