livetext 0.9.52 → 0.9.56

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/imports/bookish.rb +3 -3
  3. data/lib/livetext/ast/show_ast_clean.rb +10 -0
  4. data/lib/livetext/ast/show_ast_result.rb +60 -0
  5. data/lib/livetext/ast/show_raw_arrays.rb +13 -0
  6. data/lib/livetext/ast.rb +464 -0
  7. data/lib/livetext/ast_to_html.rb +32 -0
  8. data/lib/livetext/core.rb +110 -53
  9. data/lib/livetext/errors.rb +1 -0
  10. data/lib/livetext/expansion.rb +21 -21
  11. data/lib/livetext/formatter.rb +70 -200
  12. data/lib/livetext/formatter_component.rb +189 -0
  13. data/lib/livetext/function_registry.rb +163 -0
  14. data/lib/livetext/functions.rb +26 -0
  15. data/lib/livetext/handler/mixin.rb +53 -0
  16. data/lib/livetext/helpers.rb +33 -16
  17. data/lib/livetext/reopen.rb +2 -0
  18. data/lib/livetext/skeleton.rb +0 -3
  19. data/lib/livetext/standard.rb +120 -72
  20. data/lib/livetext/userapi.rb +20 -1
  21. data/lib/livetext/variable_manager.rb +78 -0
  22. data/lib/livetext/variables.rb +9 -1
  23. data/lib/livetext/version.rb +1 -1
  24. data/lib/livetext.rb +9 -3
  25. data/plugin/booktool.rb +14 -14
  26. data/plugin/lt3scriptor.rb +914 -0
  27. data/plugin/mixin_functions_class.rb +33 -0
  28. data/test/snapshots/complex_body/expected-error.txt +0 -0
  29. data/test/snapshots/complex_body/expected-output.txt +8 -0
  30. data/test/snapshots/complex_body/source.lt3 +19 -0
  31. data/test/snapshots/debug_command/expected-error.txt +0 -0
  32. data/test/snapshots/debug_command/expected-output.txt +1 -0
  33. data/test/snapshots/debug_command/source.lt3 +3 -0
  34. data/test/snapshots/def_parameters/expected-error.txt +0 -0
  35. data/test/snapshots/def_parameters/expected-output.txt +21 -0
  36. data/test/snapshots/def_parameters/source.lt3 +44 -0
  37. data/test/snapshots/error_missing_end/match-error.txt +1 -1
  38. data/test/snapshots/functions_reflection/expected-error.txt +0 -0
  39. data/test/snapshots/functions_reflection/expected-output.txt +27 -0
  40. data/test/snapshots/functions_reflection/source.lt3 +5 -0
  41. data/test/snapshots/mixin_functions_class/expected-error.txt +0 -0
  42. data/test/snapshots/mixin_functions_class/expected-output.txt +20 -0
  43. data/test/snapshots/mixin_functions_class/mixin_functions_class.rb +33 -0
  44. data/test/snapshots/mixin_functions_class/source.lt3 +17 -0
  45. data/test/snapshots/multiple_functions/expected-error.txt +0 -0
  46. data/test/snapshots/multiple_functions/expected-output.txt +5 -0
  47. data/test/snapshots/multiple_functions/source.lt3 +16 -0
  48. data/test/snapshots/nested_includes/expected-error.txt +0 -0
  49. data/test/snapshots/nested_includes/expected-output.txt +68 -0
  50. data/test/snapshots/nested_includes/level2.inc +34 -0
  51. data/test/snapshots/nested_includes/level3.inc +20 -0
  52. data/test/snapshots/nested_includes/source.lt3 +49 -0
  53. data/test/snapshots/parameter_handling/expected-error.txt +0 -0
  54. data/test/snapshots/parameter_handling/expected-output.txt +7 -0
  55. data/test/snapshots/parameter_handling/source.lt3 +10 -0
  56. data/test/snapshots/subset.txt +1 -0
  57. data/test/snapshots/system_info/expected-error.txt +0 -0
  58. data/test/snapshots/system_info/match-output.txt +18 -0
  59. data/test/snapshots/system_info/source.lt3 +16 -0
  60. data/test/unit/all.rb +7 -0
  61. data/test/unit/ast.rb +90 -0
  62. data/test/unit/ast_directives.rb +104 -0
  63. data/test/unit/ast_variables.rb +71 -0
  64. data/test/unit/core_methods.rb +317 -0
  65. data/test/unit/formatter.rb +84 -0
  66. data/test/unit/formatter_component.rb +84 -0
  67. data/test/unit/function_registry.rb +132 -0
  68. data/test/unit/mixin_functions_class.rb +131 -0
  69. data/test/unit/stringparser.rb +14 -32
  70. data/test/unit/variable_manager.rb +71 -0
  71. metadata +51 -5
  72. data/imports/markdown.rb +0 -44
  73. data/lib/livetext/processor.rb +0 -88
  74. data/plugin/markdown.rb +0 -43
@@ -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
@@ -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
@@ -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,54 @@ 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
+
60
+ # Also look for methods defined in Livetext::Functions class
61
+ # Get all methods from Livetext::Functions
62
+ functions_class_methods = Livetext::Functions.instance_methods(false)
63
+
64
+ functions_class_methods.each do |method_name|
65
+ # Skip methods that are already built-in (defined in the original functions.rb)
66
+ builtin_methods = [:code_lines, :ns, :isqrt, :reverse, :date, :time, :pwd, :rand, :link, :br, :yt, :simple_format,
67
+ :b, :i, :t, :s, :bi, :bt, :bs, :it, :is, :ts, :bit, :bis, :bts, :its, :bits]
68
+ next if builtin_methods.include?(method_name)
69
+
70
+ # Create a lambda that calls the method on a new Livetext::Functions instance
71
+ function = ->(param) do
72
+ fobj = ::Livetext::Functions.new
73
+ # Set the Livetext instance and its variables for access in functions
74
+ fobj.live = parent
75
+ fobj.vars = parent.vars
76
+ method = fobj.method(method_name)
77
+ if method.parameters.empty?
78
+ fobj.send(method_name)
79
+ else
80
+ fobj.send(method_name, param)
81
+ end
82
+ end
83
+
84
+ # Register with the function registry
85
+ parent.function_registry.register_user(method_name.to_s, function, source: :mixin, filename: @file)
86
+ end
87
+ end
88
+
36
89
  end
37
90
 
@@ -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
@@ -122,25 +122,42 @@ module Livetext::Helpers
122
122
  end
123
123
 
124
124
  def invoke_dotcmd(name, data0="")
125
- api.data = data0.dup # should permit _ in function names at least
125
+ api.data = data0.dup
126
126
  args0 = data0.split
127
127
  api.args = args0.dup
128
- retval = @main.send(name) # , *args) # was 125
128
+ method = method(name)
129
+ params = method.parameters
130
+
131
+ case params.length
132
+ when 0
133
+ retval = send(name)
134
+ when 2
135
+ retval = send(name, args0, data0)
136
+ when 3
137
+ processed_body = api.body(false)
138
+ retval = send(name, args0, data0, processed_body)
139
+ when 4
140
+ if params[3][1] == :raw
141
+ raw_body = api.body(true)
142
+ retval = send(name, args0, data0, raw_body, true)
143
+ else
144
+ raise "4th parameter is not :raw!"
145
+ end
146
+ else
147
+ raise "#{name} has #{params.length} parameters!"
148
+ end
129
149
  retval
130
- rescue => err
131
- graceful_error(err) # , "#{__method__}: name = #{name}")
132
150
  end
133
151
 
134
152
  def handle_dotcmd(line, indent = 0)
135
- indent = @indentation.last # top of stack
136
- line = line.sub(/# .*$/, "") # FIXME Could be problematic?
153
+ line = line.sub(/# .*$/, "")
137
154
  name, data = get_name_data(line)
138
- success = true # Be optimistic... :P
155
+ check_disallowed(name)
139
156
  case
140
- when name == :end # special case
141
- graceful_error EndWithoutOpening()
142
- when @main.respond_to?(name)
143
- success = invoke_dotcmd(name, data) # was 141
157
+ when name == :end # special case
158
+ graceful_error EndWithoutOpening()
159
+ when respond_to?(name)
160
+ success = invoke_dotcmd(name, data) # was 141
144
161
  else
145
162
  graceful_error UnknownMethod(name)
146
163
  end
@@ -162,8 +179,8 @@ module Livetext::Helpers
162
179
  data0 = ""
163
180
  end
164
181
  name = "dot_" + name if %w[include def].include?(name)
165
- @main.check_disallowed(name)
166
- @main.api.data = data0 # FIXME kill this?
182
+ check_disallowed(name)
183
+ api.data = data0 # FIXME kill this?
167
184
  [name.to_sym, data0]
168
185
  end
169
186
 
@@ -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