livetext 0.9.51 → 0.9.55

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/imports/bookish.rb +3 -3
  3. data/lib/livetext/core.rb +108 -53
  4. data/lib/livetext/expansion.rb +25 -9
  5. data/lib/livetext/formatter.rb +149 -162
  6. data/lib/livetext/formatter_component.rb +189 -0
  7. data/lib/livetext/function_registry.rb +163 -0
  8. data/lib/livetext/handler/mixin.rb +25 -0
  9. data/lib/livetext/helpers.rb +25 -7
  10. data/lib/livetext/reopen.rb +2 -0
  11. data/lib/livetext/skeleton.rb +0 -3
  12. data/lib/livetext/standard.rb +118 -70
  13. data/lib/livetext/variable_manager.rb +65 -0
  14. data/lib/livetext/variables.rb +4 -0
  15. data/lib/livetext/version.rb +1 -1
  16. data/lib/livetext.rb +9 -3
  17. data/plugin/booktool.rb +8 -8
  18. data/test/extra/testgen.rb +1 -3
  19. data/test/snapshots/complex_body/expected-error.txt +0 -0
  20. data/test/snapshots/complex_body/expected-output.txt +8 -0
  21. data/test/snapshots/complex_body/source.lt3 +19 -0
  22. data/test/snapshots/debug_command/expected-error.txt +0 -0
  23. data/test/snapshots/debug_command/expected-output.txt +1 -0
  24. data/test/snapshots/debug_command/source.lt3 +3 -0
  25. data/test/snapshots/def_parameters/expected-error.txt +0 -0
  26. data/test/snapshots/def_parameters/expected-output.txt +21 -0
  27. data/test/snapshots/def_parameters/source.lt3 +44 -0
  28. data/test/snapshots/functions_reflection/expected-error.txt +0 -0
  29. data/test/snapshots/functions_reflection/expected-output.txt +27 -0
  30. data/test/snapshots/functions_reflection/source.lt3 +5 -0
  31. data/test/snapshots/mixin_functions/expected-error.txt +0 -0
  32. data/test/snapshots/mixin_functions/expected-output.txt +18 -0
  33. data/test/snapshots/mixin_functions/mixin_functions.rb +11 -0
  34. data/test/snapshots/mixin_functions/source.lt3 +15 -0
  35. data/test/snapshots/multiple_functions/expected-error.txt +0 -0
  36. data/test/snapshots/multiple_functions/expected-output.txt +5 -0
  37. data/test/snapshots/multiple_functions/source.lt3 +16 -0
  38. data/test/snapshots/nested_includes/expected-error.txt +0 -0
  39. data/test/snapshots/nested_includes/expected-output.txt +68 -0
  40. data/test/snapshots/nested_includes/level2.inc +34 -0
  41. data/test/snapshots/nested_includes/level3.inc +20 -0
  42. data/test/snapshots/nested_includes/source.lt3 +49 -0
  43. data/test/snapshots/parameter_handling/expected-error.txt +0 -0
  44. data/test/snapshots/parameter_handling/expected-output.txt +7 -0
  45. data/test/snapshots/parameter_handling/source.lt3 +10 -0
  46. data/test/snapshots/subset.txt +1 -0
  47. data/test/snapshots/system_info/expected-error.txt +0 -0
  48. data/test/snapshots/system_info/expected-output.txt +18 -0
  49. data/test/snapshots/system_info/source.lt3 +16 -0
  50. data/test/snapshots.rb +3 -4
  51. data/test/test_escape.lt3 +1 -0
  52. data/test/unit/all.rb +4 -0
  53. data/test/unit/bracketed.rb +2 -2
  54. data/test/unit/core_methods.rb +137 -0
  55. data/test/unit/double.rb +1 -3
  56. data/test/unit/formatter.rb +84 -0
  57. data/test/unit/formatter_component.rb +84 -0
  58. data/test/unit/function_registry.rb +132 -0
  59. data/test/unit/functions.rb +2 -2
  60. data/test/unit/html.rb +2 -2
  61. data/test/unit/parser/general.rb +2 -2
  62. data/test/unit/parser/mixin.rb +2 -2
  63. data/test/unit/parser/set.rb +2 -2
  64. data/test/unit/parser/string.rb +2 -2
  65. data/test/unit/single.rb +1 -3
  66. data/test/unit/standard.rb +1 -3
  67. data/test/unit/stringparser.rb +2 -2
  68. data/test/unit/variable_manager.rb +71 -0
  69. data/test/unit/variables.rb +2 -2
  70. metadata +41 -3
  71. data/lib/livetext/processor.rb +0 -88
@@ -0,0 +1,189 @@
1
+ # Formatter - Centralized text formatting for Livetext
2
+ class Livetext::Formatter
3
+ def initialize(parent)
4
+ @parent = parent
5
+ end
6
+
7
+ def format(text)
8
+ return "" if text.nil? || text.empty?
9
+
10
+ text = text.chomp
11
+ # First, mark escaped characters so they won't be processed as formatting
12
+ text = mark_escaped_characters(text)
13
+
14
+ # Process all marker types in sequence
15
+ text = handle_double_markers(text)
16
+ text = handle_bracketed_markers(text)
17
+ text = handle_single_markers(text)
18
+ text = handle_underscore_markers(text)
19
+ text = handle_backtick_markers(text)
20
+ text = handle_tilde_markers(text)
21
+
22
+ text = unmark_escaped_characters(text)
23
+ text
24
+ end
25
+
26
+ def format_line(line)
27
+ return "" if line.nil?
28
+ format(line.chomp)
29
+ end
30
+
31
+ def format_multiple(lines)
32
+ lines.map { |line| format_line(line) }
33
+ end
34
+
35
+ # Convenience methods for common formatting patterns
36
+ def bold(text)
37
+ "<b>#{text}</b>"
38
+ end
39
+
40
+ def italic(text)
41
+ "<i>#{text}</i>"
42
+ end
43
+
44
+ def code(text)
45
+ "<tt>#{text}</tt>"
46
+ end
47
+
48
+ def strike(text)
49
+ "<strike>#{text}</strike>"
50
+ end
51
+
52
+ def link(text, url)
53
+ "<a href='#{url}'>#{text}</a>"
54
+ end
55
+
56
+ def escape_html(text)
57
+ text.gsub(/[&<>"']/) do |char|
58
+ case char
59
+ when '&' then '&amp;'
60
+ when '<' then '&lt;'
61
+ when '>' then '&gt;'
62
+ when '"' then '&quot;'
63
+ when "'" then '&#39;'
64
+ else char
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def mark_escaped_characters(str)
72
+ # Replace escaped characters with a null byte marker (safe for internal use)
73
+ str.gsub(/\\([*_`~])/, "\u0000\\1")
74
+ end
75
+
76
+ def unmark_escaped_characters(str)
77
+ # Restore escaped characters
78
+ str.gsub(/\u0000([*_`~])/, '\1')
79
+ end
80
+
81
+ def handle_double_markers(str)
82
+ # **word -> <b>word</b> (terminated by space, comma, period)
83
+ # But ignore standalone ** or ** surrounded by spaces
84
+ str.gsub(/(?<=\s|^)\*\*([^\s,.]*)/) do |match|
85
+ if $1.empty?
86
+ "**" # standalone ** should be literal
87
+ else
88
+ "<b>#{$1}</b>"
89
+ end
90
+ end
91
+ end
92
+
93
+ def handle_bracketed_markers(str)
94
+ # Handle all bracketed markers: *[content], _[content], `[content]
95
+ # *[content] -> <b>content</b>
96
+ # _[content] -> <i>content</i>
97
+ # `[content] -> <tt>content</tt>
98
+ # And handle unclosed brackets with end-of-line termination
99
+ # Empty brackets disappear
100
+ # But ignore if the marker was originally escaped
101
+ # And ignore embedded markers (like abc*[)
102
+
103
+ # First handle complete brackets for all marker types
104
+ str = str.gsub(/(?<!\u0000)([*_`])\[([^\]]*)\]/) do |match|
105
+ marker, content = $1, $2
106
+ if content.empty?
107
+ "" # empty brackets disappear
108
+ else
109
+ case marker
110
+ when "*" then "<b>#{content}</b>"
111
+ when "_" then "<i>#{content}</i>"
112
+ when "`" then "<tt>#{content}</tt>"
113
+ else match # fallback
114
+ end
115
+ end
116
+ end
117
+
118
+ # Then handle unclosed brackets (end of line replaces closing bracket)
119
+ # But only if it's at start of line or preceded by whitespace
120
+ str = str.gsub(/(?<!\u0000)(?<=\s|^)([*_`])\[([^\]]*)$/) do |match|
121
+ marker, content = $1, $2
122
+ if content.empty?
123
+ "" # standalone marker[ disappears
124
+ else
125
+ case marker
126
+ when "*" then "<b>#{content}</b>"
127
+ when "_" then "<i>#{content}</i>"
128
+ when "`" then "<tt>#{content}</tt>"
129
+ else match # fallback
130
+ end
131
+ end
132
+ end
133
+
134
+ str
135
+ end
136
+
137
+ def handle_single_markers(str)
138
+ # *word -> <b>word</b> (only at start of word or after space)
139
+ # But ignore standalone * or * surrounded by spaces
140
+ # Also ignore * that are part of ** patterns (already processed)
141
+ # And ignore * that are part of *[ patterns (already processed)
142
+ str.gsub(/(?<=\s|^)\*(?!\[)([^\s]*)/) do |match|
143
+ if $1.empty?
144
+ "*" # standalone * should be literal
145
+ elsif $1.start_with?('*')
146
+ # This is part of a ** pattern, leave it as literal
147
+ match
148
+ else
149
+ "<b>#{$1}</b>"
150
+ end
151
+ end
152
+ end
153
+
154
+ def handle_underscore_markers(str)
155
+ # _word -> <i>word</i> (only at start of word or after space)
156
+ # But ignore standalone _ or _ surrounded by spaces
157
+ str.gsub(/(?<=\s|^)_([^\s]*)/) do |match|
158
+ if $1.empty?
159
+ "_" # standalone _ should be literal
160
+ else
161
+ "<i>#{$1}</i>"
162
+ end
163
+ end
164
+ end
165
+
166
+ def handle_backtick_markers(str)
167
+ # `word -> <tt>word</tt> (only at start of word or after space)
168
+ # But ignore standalone ` or ` surrounded by spaces
169
+ str.gsub(/(?<=\s|^)`([^\s]*)/) do |match|
170
+ if $1.empty?
171
+ "`" # standalone ` should be literal
172
+ else
173
+ "<tt>#{$1}</tt>"
174
+ end
175
+ end
176
+ end
177
+
178
+ def handle_tilde_markers(str)
179
+ # ~word -> <strike>word</strike> (only at start of word or after space)
180
+ # But ignore standalone ~ or ~ surrounded by spaces
181
+ str.gsub(/(?<=\s|^)~([^\s]*)/) do |match|
182
+ if $1.empty?
183
+ "~" # standalone ~ should be literal
184
+ else
185
+ "<strike>#{$1}</strike>"
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,163 @@
1
+ # Function Registry - Unified function management for Livetext
2
+ class Livetext::FunctionRegistry
3
+ def initialize
4
+ @user_functions = {}
5
+ @builtin_functions = {}
6
+ @metadata = {}
7
+ register_builtin_functions
8
+ puts "DEBUG: Registered #{@builtin_functions.size} builtin functions" if ENV['DEBUG']
9
+ end
10
+
11
+ def register_user(name, function, source: :inline, filename: nil)
12
+ name_sym = name.to_sym
13
+ @user_functions[name_sym] = function
14
+ @metadata[name_sym] = { source: source, filename: filename }
15
+ end
16
+
17
+ def register_builtin(name, function)
18
+ @builtin_functions[name] = function
19
+ @metadata[name] = { source: :builtin, filename: "builtin" }
20
+ end
21
+
22
+ def call(name, param)
23
+ # Convert name to symbol for consistent lookup
24
+ name_sym = name.to_sym
25
+
26
+ if @user_functions[name_sym]
27
+ call_function(name, @user_functions[name_sym], param)
28
+ elsif @builtin_functions[name_sym]
29
+ call_function(name, @builtin_functions[name_sym], param)
30
+ else
31
+ "[Error evaluating $$#{name}(#{param})]"
32
+ end
33
+ end
34
+
35
+ def list_functions
36
+ result = []
37
+ @user_functions.each { |name, _| result << { name: name.to_s, source: get_source(name) } }
38
+ @builtin_functions.each { |name, _| result << { name: name.to_s, source: get_source(name) } }
39
+ result.sort_by { |f| f[:name] }
40
+ end
41
+
42
+ def get_function_info(name)
43
+ name_sym = name.to_sym
44
+ metadata = @metadata[name_sym]
45
+ return nil unless metadata
46
+
47
+ {
48
+ name: name.to_s,
49
+ source: get_source(name),
50
+ filename: metadata[:filename],
51
+ type: @user_functions.key?(name_sym) ? :user : :builtin
52
+ }
53
+ end
54
+
55
+ def function_exists?(name)
56
+ name_sym = name.to_sym
57
+ @user_functions.key?(name_sym) || @builtin_functions.key?(name_sym)
58
+ end
59
+
60
+ private
61
+
62
+ def call_function(name, function, param)
63
+ function.call(param)
64
+ rescue => e
65
+ "[Error evaluating $$#{name}(#{param})]"
66
+ end
67
+
68
+ def get_source(name)
69
+ name_sym = name.to_sym
70
+ metadata = @metadata[name_sym]
71
+ case metadata[:source]
72
+ when :inline then "inline (#{metadata[:filename] || 'unknown'})"
73
+ when :mixin then "mixin (#{metadata[:filename] || 'unknown'})"
74
+ when :builtin then "builtin"
75
+ else "unknown"
76
+ end
77
+ end
78
+
79
+ def register_builtin_functions
80
+ # Move all current Livetext::Functions methods here
81
+ register_builtin(:date, ->(param) { Time.now.strftime("%F") })
82
+ register_builtin(:time, ->(param) { Time.now.strftime("%T") })
83
+ register_builtin(:pwd, ->(param) { Dir.pwd })
84
+ register_builtin(:rand, ->(param) do
85
+ if param && !param.empty?
86
+ n1, n2 = param.split.map(&:to_i)
87
+ Kernel.rand(n1..n2).to_s
88
+ else
89
+ Kernel.rand.to_s
90
+ end
91
+ end)
92
+ register_builtin(:link, ->(param) do
93
+ if param && param.include?("|")
94
+ text, url = param.split("|", 2)
95
+ "<a style='text-decoration: none' href='#{url}'>#{text}</a>"
96
+ else
97
+ "[Error in function $$link: expected 'text|url' format]"
98
+ end
99
+ end)
100
+ register_builtin(:br, ->(param) do
101
+ n = (param || "1").to_i
102
+ "<br>" * n
103
+ end)
104
+ register_builtin(:reverse, ->(param) do
105
+ if param && !param.empty?
106
+ param.reverse
107
+ else
108
+ "(reverse: No parameter)"
109
+ end
110
+ end)
111
+ register_builtin(:isqrt, ->(param) do
112
+ arg = num = param
113
+ if num.nil? || num.empty?
114
+ arg = "NO PARAM"
115
+ end
116
+ num = num.include?(".") ? Float(num) : Integer(num)
117
+ Math.sqrt(num).to_i.to_s
118
+ rescue => err
119
+ "[Error evaluating $$isqrt(#{arg})]"
120
+ end)
121
+
122
+ # System info functions (return current values)
123
+ register_builtin(:hostname, ->(param) { `hostname`.chomp })
124
+ register_builtin(:platform, ->(param) { RUBY_PLATFORM })
125
+ register_builtin(:ruby_version, ->(param) { RUBY_VERSION })
126
+ register_builtin(:livetext_version, ->(param) { Livetext::VERSION })
127
+
128
+ # Date/time functions
129
+ register_builtin(:year, ->(param) { Time.now.year.to_s })
130
+ register_builtin(:month, ->(param) { Time.now.mon.to_s })
131
+ register_builtin(:day, ->(param) { Time.now.day.to_s })
132
+ register_builtin(:hour, ->(param) { Time.now.hour.to_s })
133
+ register_builtin(:minute, ->(param) { Time.now.min.to_s })
134
+ register_builtin(:second, ->(param) { Time.now.sec.to_s })
135
+ register_builtin(:weekday, ->(param) { Time.now.wday.to_s })
136
+ register_builtin(:week, ->(param) { Time.now.strftime("%U") })
137
+
138
+ # Date formatting functions
139
+ register_builtin(:format_date, ->(param) do
140
+ if param && !param.empty?
141
+ Time.now.strftime(param)
142
+ else
143
+ Time.now.strftime("%F") # Default format
144
+ end
145
+ end)
146
+ register_builtin(:days_ago, ->(param) do
147
+ if param && !param.empty?
148
+ days = param.to_i
149
+ (Time.now - (days * 24 * 60 * 60)).strftime("%F")
150
+ else
151
+ "[Error evaluating $$days_ago: requires number of days]"
152
+ end
153
+ end)
154
+ register_builtin(:days_from_now, ->(param) do
155
+ if param && !param.empty?
156
+ days = param.to_i
157
+ (Time.now + (days * 24 * 60 * 60)).strftime("%F")
158
+ else
159
+ "[Error evaluating $$days_from_now: requires number of days]"
160
+ end
161
+ end)
162
+ end
163
+ end
@@ -24,6 +24,10 @@ class Livetext::Handler::Mixin
24
24
  # STDERR.puts "After eval"
25
25
  newmod = Object.const_get("::" + modname)
26
26
  # STDERR.puts "After const_get"
27
+
28
+ # Register functions from the mixin with the registry
29
+ handler.register_mixin_functions(newmod, parent)
30
+
27
31
  newmod # return actual module
28
32
  end
29
33
 
@@ -33,5 +37,26 @@ class Livetext::Handler::Mixin
33
37
  [modname, "module ::#{modname}; #{meths}\nend"]
34
38
  end
35
39
 
40
+ def register_mixin_functions(module_obj, parent)
41
+ # Get all instance methods from the module
42
+ methods = module_obj.instance_methods(false)
43
+
44
+ methods.each do |method_name|
45
+ # Create a lambda that calls the method on the parent (Livetext instance)
46
+ function = ->(param) do
47
+ # Check if the method expects parameters
48
+ method = parent.method(method_name)
49
+ if method.parameters.empty?
50
+ parent.send(method_name)
51
+ else
52
+ parent.send(method_name, param)
53
+ end
54
+ end
55
+
56
+ # Register with the function registry
57
+ parent.function_registry.register_user(method_name.to_s, function, source: :mixin, filename: @file)
58
+ end
59
+ end
60
+
36
61
  end
37
62
 
@@ -81,15 +81,15 @@ module Livetext::Helpers
81
81
  text = File.readlines(fname)
82
82
  enum = text.each
83
83
  @backtrace = btrace
84
- @main.source(enum, fname, 0)
84
+ source(enum, fname, 0)
85
85
  line = nil
86
86
  loop do
87
- line = @main.nextline
87
+ line = nextline
88
88
  break if line.nil?
89
89
  success = process_line(line)
90
90
  break unless success
91
91
  end
92
- val = @main.finalize rescue nil
92
+ val = finalize rescue nil
93
93
  @body # FIXME? @body.join("\n") # array
94
94
  return true
95
95
  end
@@ -125,7 +125,25 @@ module Livetext::Helpers
125
125
  api.data = data0.dup # should permit _ in function names at least
126
126
  args0 = data0.split
127
127
  api.args = args0.dup
128
- retval = @main.send(name) # , *args) # was 125
128
+ # Get method signature to determine what parameters to pass
129
+ method = method(name)
130
+ param_count = method.parameters.length
131
+
132
+ # Pass parameters based on method signature
133
+ case param_count
134
+ when 0
135
+ retval = send(name)
136
+ when 2
137
+ retval = send(name, args0, data0)
138
+ when 3
139
+ # Check if this is a method that needs raw body content
140
+ # For now, we'll check if it's dot_def and if it has 'body raw' in args
141
+ raw_body = (name == :dot_def && args0.length >= 3 && args0[2] == 'raw')
142
+ body_lines = raw_body ? api.body(true) : api.body(false)
143
+ retval = send(name, args0, data0, body_lines)
144
+ else
145
+ retval = send(name) # fallback to no parameters
146
+ end
129
147
  retval
130
148
  rescue => err
131
149
  graceful_error(err) # , "#{__method__}: name = #{name}")
@@ -139,7 +157,7 @@ module Livetext::Helpers
139
157
  case
140
158
  when name == :end # special case
141
159
  graceful_error EndWithoutOpening()
142
- when @main.respond_to?(name)
160
+ when respond_to?(name)
143
161
  success = invoke_dotcmd(name, data) # was 141
144
162
  else
145
163
  graceful_error UnknownMethod(name)
@@ -162,8 +180,8 @@ module Livetext::Helpers
162
180
  data0 = ""
163
181
  end
164
182
  name = "dot_" + name if %w[include def].include?(name)
165
- @main.check_disallowed(name)
166
- @main.api.data = data0 # FIXME kill this?
183
+ check_disallowed(name)
184
+ api.data = data0 # FIXME kill this?
167
185
  [name.to_sym, data0]
168
186
  end
169
187
 
@@ -1,5 +1,7 @@
1
1
  # Reopen for convenience... do differently
2
2
 
3
+
4
+
3
5
  class Object
4
6
  def send?(meth, *args)
5
7
  if self.respond_to?(meth)
@@ -9,9 +9,6 @@ class Livetext
9
9
  module Handler
10
10
  end
11
11
 
12
- module Formatter
13
- end
14
-
15
12
  class HTML
16
13
  end
17
14