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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1d0ef585b57b9cf3ffc439ab8dc44d67ba5b287219920121694d06a78558872
4
- data.tar.gz: a73d887e9f053977c1c96b0b7ac8b1eb4bd8976180e5fee18ecb5ef5b5ca1e65
3
+ metadata.gz: 00c147a97637c967777f1a1a8bae52d8e0ca8e65f3e032e9a03f0f3bbc2c5d50
4
+ data.tar.gz: c931920c669cf05c76176d19bcfb9c06149d3bf6b32167aa2e6da7b50290adef
5
5
  SHA512:
6
- metadata.gz: 4ceda09db8d0819b550c485af5fb3d5f6da2be17ce879a07c1199c31554f76550bebaf8bcc77e834ff20466eb859d3712996fd23d0c6812aba799809f6a66ada
7
- data.tar.gz: 97c76c0a601541d78afa39da976fb966ee3a7917c065eb5589c70b755e8120f89916edbc7eacc72f02c4f76edcbadd839ed545e482b097e970e87602b59b4ecf
6
+ metadata.gz: 59836de9481a1239cf0c0f00f55681da634c7e96e337c67d2c4fb8974ec3dad06fa83bbe7eb7f73596c73d97e9359aab613efbf87d0ddfe70e9a3a50c65e931e
7
+ data.tar.gz: b0e1a74d2d75e8c47afff0881594d1022dbb63e71ec67b8304622b9885bc62429d6782286482a45520587207fc48e4158d0a857f51bcac5596c51cc779f2cf01
data/imports/bookish.rb CHANGED
@@ -205,7 +205,7 @@ api.tty "## sec: cp 4"
205
205
  title = api.data
206
206
  delim = " :: "
207
207
  api.out "<br><center><table width=90% cellpadding=5>"
208
- lines = api.body(true)
208
+ lines = body
209
209
  maxw = nil
210
210
  lines.each do |line|
211
211
  api.format(line)
@@ -238,8 +238,8 @@ api.tty "## sec: cp 4"
238
238
  _debug "Closing TOC"
239
239
  @toc.close
240
240
  rescue => err
241
- puts @parent.body
242
- @parent.body = ""
241
+ puts self.body
242
+ self.body = ""
243
243
  _errout "Exception: #{err.inspect}"
244
244
  end
245
245
 
data/lib/livetext/core.rb CHANGED
@@ -8,7 +8,23 @@ class Livetext
8
8
 
9
9
  TTY = ::File.open("/dev/tty", "w")
10
10
 
11
- attr_reader :main, :sources
11
+ Disallowed =
12
+ %i[ __binding__ __id__ __send__ class
13
+ clone display dup enum_for
14
+ eql? equal? extend freeze
15
+ frozen? hash inspect instance_eval
16
+ instance_exec instance_of? is_a? kind_of?
17
+ method methods nil? object_id
18
+ pretty_inspect private_methods protected_methods public_method
19
+ public_methods public_send respond_to? send
20
+ singleton_class singleton_method singleton_methods taint
21
+ tainted? tap to_enum to_s
22
+ trust untaint untrust untrusted?
23
+ define_singleton_method instance_variable_defined?
24
+ instance_variable_get instance_variable_set
25
+ remove_instance_variable instance_variables ]
26
+
27
+ attr_reader :sources, :function_registry, :variables, :formatter
12
28
  attr_accessor :nopass, :nopara
13
29
  attr_accessor :body, :indentation
14
30
 
@@ -17,7 +33,7 @@ class Livetext
17
33
  end
18
34
 
19
35
  def vars
20
- @_vars
36
+ @variables
21
37
  end
22
38
 
23
39
  def self.interpolate(str)
@@ -27,16 +43,10 @@ class Livetext
27
43
  str3
28
44
  end
29
45
 
30
- def peek_nextline
31
- @main.peek_nextline # delegate
32
- end
33
46
 
34
- def nextline
35
- @main.nextline # delegate
36
- end
37
47
 
38
48
  def sources
39
- @main.sources # delegate
49
+ @sources # delegate
40
50
  end
41
51
 
42
52
  def save_location
@@ -48,18 +58,15 @@ class Livetext
48
58
  end
49
59
 
50
60
  def initialize(output = ::STDOUT) # Livetext
51
- @source = nil
52
- @_mixins = []
53
- @_imports = []
54
- @_outdir = "."
55
- @no_puts = output.nil?
56
61
  @body = ""
57
- @main = Processor.new(self, output) # nil = make @main its own parent??
58
- @parent = @main
59
62
  @indentation = [0]
60
- @_vars = Livetext::Vars
61
63
  @api = UserAPI.new(self)
62
- initial_vars
64
+ @output = ::Livetext.output = output
65
+ @html = Livetext::HTML.new(@api)
66
+ @sources = []
67
+ @function_registry = Livetext::FunctionRegistry.new
68
+ @variables = Livetext::VariableManager.new(self)
69
+ @formatter = Livetext::Formatter.new(self)
63
70
  # puts "------ init: self = "
64
71
  # p self
65
72
  end
@@ -71,7 +78,7 @@ class Livetext
71
78
  mix.each do |lib|
72
79
  obj.invoke_dotcmd(:mixin, lib.dup)
73
80
  end
74
- call.each {|cmd| obj.main.send(cmd[1..-1]) } # ignores leading dot, no param
81
+ call.each {|cmd| obj.send(cmd[1..-1]) } # ignores leading dot, no param
75
82
  obj.api.setvars(vars)
76
83
  obj
77
84
  end
@@ -80,22 +87,17 @@ class Livetext
80
87
  mix = Array(mix)
81
88
  call = Array(call)
82
89
  mix.each {|lib| mixin(lib) }
83
- call.each {|cmd| @main.send(cmd[1..-1]) } # ignores leading dot, no param
90
+ call.each {|cmd| send(cmd[1..-1]) } # ignores leading dot, no param
84
91
  # vars.each_pair {|var, val| @api.set(var, val.to_s) }
85
92
  api.setvars(vars)
86
93
  self
87
94
  end
88
95
 
89
- def inspect
96
+ def inspect
90
97
  api_abbr = @api ? "(non-nil)" : "(not shown)"
91
- main_abbr = @main ? "(non-nil)" : "(not shown)"
92
98
  "Livetext:\n" +
93
- " source = #{@source.inspect}\n" +
94
- " mixins = #{@_mixins.inspect}\n" +
95
- " import = #{@_mixins.inspect}\n" +
96
- " main = #{main_abbr}\n" +
97
99
  " indent = #{@indentation.inspect}\n" +
98
- " vars = #{@_vars.inspect}\n" +
100
+ " vars = #{@variables.inspect}\n" +
99
101
  " api = #{api_abbr}\n" +
100
102
  " body = (#{@body.size} bytes)"
101
103
  end
@@ -104,23 +106,83 @@ class Livetext
104
106
  @api
105
107
  end
106
108
 
109
+ def error(*args)
110
+ ::STDERR.puts *args
111
+ end
112
+
113
+ def disallowed?(name)
114
+ flag = Disallowed.include?(name.to_sym)
115
+ flag
116
+ end
117
+
118
+ def output=(io)
119
+ @output = io
120
+ end
121
+
122
+ def html
123
+ @html
124
+ end
125
+
126
+ def source(enum, file, line)
127
+ @sources.push([enum, file, line])
128
+ end
129
+
130
+ def peek_nextline
131
+ return nil if @sources.empty?
132
+ source = @sources.last
133
+ line = source[0].peek
134
+ line
135
+ rescue StopIteration
136
+ @sources.pop
137
+ nil
138
+ rescue => err
139
+ TTY.puts "#{__method__}: RESCUE err = #{err.inspect}"
140
+ nil
141
+ end
142
+
143
+ def nextline
144
+ return nil if @sources.empty?
145
+ line = @sources.last[0].next
146
+ @sources.last[2] += 1
147
+ line
148
+ rescue StopIteration
149
+ @sources.pop
150
+ nil
151
+ end
152
+
107
153
  def api=(obj)
108
154
  @api = obj
109
155
  end
110
156
 
111
- def initial_vars
112
- # Other predefined variables (see also setfile)
113
- @api.setvar(:User, `whoami`.chomp)
114
- @api.setvar(:Version, Livetext::VERSION)
157
+
158
+
159
+ def process(text: nil, file: nil, vars: {})
160
+ # Set variables first
161
+ @variables.set_multiple(vars) unless vars.empty?
162
+
163
+ # Process based on input type
164
+ case
165
+ when file && text.nil?
166
+ process_file(file)
167
+ when file.nil? && text
168
+ transform_text(text)
169
+ when file.nil? && text.nil?
170
+ raise "Must specify file or text"
171
+ when file && text
172
+ raise "Cannot specify file and text"
173
+ end
174
+
175
+ [self.body, @variables.to_h]
115
176
  end
116
177
 
117
- def transform(text)
178
+ # Keep transform for backward compatibility, but make it private
179
+ private def transform_text(text)
118
180
  setfile!("(string)")
119
181
  enum = text.each_line
120
182
  front = text.match(/.*?\n/).to_a.first.chomp rescue ""
121
- @main.source(enum, "STDIN: '#{front}...'", 0)
183
+ source(enum, "STDIN: '#{front}...'", 0)
122
184
  loop do
123
- line = @main.nextline
185
+ line = nextline
124
186
  break if line.nil?
125
187
  process_line(line)
126
188
  end
@@ -129,29 +191,22 @@ class Livetext
129
191
  result
130
192
  end
131
193
 
132
- # EXPERIMENTAL and incomplete
194
+ # Keep for backward compatibility
195
+ def transform(text)
196
+ transform_text(text)
197
+ end
198
+
199
+ # Keep for backward compatibility
133
200
  def xform(*args, file: nil, text: nil, vars: {})
134
- case
135
- when file && text.nil?
136
- xform_file(file)
137
- when file.nil? && text
138
- transform(text)
139
- when file.nil? && text.nil?
140
- raise "Must specify file or text"
141
- when file && text
142
- raise "Cannot specify file and text"
143
- end
144
- self.process_file(file)
145
- self.body
201
+ body, _vars = process(file: file, text: text, vars: vars)
202
+ body
146
203
  end
147
204
 
205
+ # Keep for backward compatibility
148
206
  def xform_file(file, vars: nil)
149
- Livetext::Vars.replace(vars) unless vars.nil?
150
- @_vars.replace(vars) unless vars.nil?
151
- # checkpoint! "Calling process_file..."
152
- self.process_file(file)
153
- # checkpoint! "...returned"
154
- self.body
207
+ vars_hash = vars.nil? ? {} : vars
208
+ body, _vars = process(file: file, vars: vars_hash)
209
+ body
155
210
  end
156
211
 
157
212
  end
@@ -8,8 +8,6 @@ class Livetext::Expansion
8
8
  Lbrack = "\\["
9
9
  Colon = ":"
10
10
 
11
- Formatter = ::Livetext::Formatter
12
-
13
11
  def initialize(instance) # Livetext::Expansion
14
12
  @live = instance
15
13
  end
@@ -18,7 +16,7 @@ class Livetext::Expansion
18
16
  return "" if line == "\n" || line.nil?
19
17
  with_vars = expand_variables(line)
20
18
  with_func = expand_function_calls(with_vars)
21
- formatted = Formatter.format(with_func)
19
+ formatted = @live.formatter.format(with_func)
22
20
  end
23
21
 
24
22
  def expand_variables(str)
@@ -53,12 +51,30 @@ class Livetext::Expansion
53
51
  end
54
52
 
55
53
  def funcall(name, param)
56
- err = "[Error evaluating $$#{name}(#{param})]"
54
+ # Use the unified function registry
57
55
  name = name.gsub(/\./, "__")
58
- return if self.send?(name, param)
59
- fobj = ::Livetext::Functions.new
60
- result = fobj.send(name, param) rescue err
61
- result.to_s
56
+ result = @live.function_registry.call(name, param)
57
+
58
+ # If not found in registry, fall back to old system for backward compatibility
59
+ if result.start_with?("[Error evaluating $$#{name}(")
60
+ # Try old Livetext::Functions system
61
+ fobj = ::Livetext::Functions.new
62
+ old_result = fobj.send(name, param) rescue nil
63
+ return old_result.to_s if old_result
64
+
65
+ # Try Livetext instance (for mixin functions)
66
+ if @live.respond_to?(name)
67
+ method = @live.method(name)
68
+ if method.parameters.empty?
69
+ old_result = @live.send(name) rescue nil
70
+ else
71
+ old_result = @live.send(name, param) rescue nil
72
+ end
73
+ return old_result.to_s if old_result
74
+ end
75
+ end
76
+
77
+ result
62
78
  end
63
79
 
64
80
  def expand_function_calls(str)
@@ -69,7 +85,7 @@ class Livetext::Expansion
69
85
  rbrack = "\\]"
70
86
  space_eol = "( |$)"
71
87
  prx1 = "(?<param>[^ ]+)"
72
- prx2 = "(?<param>.+)"
88
+ prx2 = "(?<param>.*)"
73
89
  pat2 = "(?<full_param>#{colon}#{prx1})"
74
90
  pat3 = "(?<full_param>#{lbrack}#{prx2}#{rbrack})"
75
91
  rx = Regexp.compile("#{pat1}(#{pat2}|#{pat3})?")
@@ -1,202 +1,189 @@
1
- module Livetext::Formatter
2
-
3
- def self.format(str) # FIXME - unneeded?
4
- str = str.chomp
5
- s2 = Double.process(str.chomp)
6
- s3 = Bracketed.process(s2)
7
- s4 = Single.process(s3)
8
- s4
9
- end
10
-
11
- ## Hmmm...
12
- #
13
- # Double: b, i, t, s
14
- # Single: bits
15
- # Brackt: bits
16
- #
17
-
18
- end
19
-
20
- class Livetext::Formatter::Delimited
21
- def initialize(str, marker, tag) # Delimited
22
- @str, @marker, @tag = str.dup, marker, tag
23
- @buffer = ""
24
- @cdata = ""
25
- @state = :INITIAL
1
+ # Formatter - Centralized text formatting for Livetext
2
+ class Livetext::Formatter
3
+ def initialize(parent)
4
+ @parent = parent
26
5
  end
27
6
 
28
- def status(where)
29
- if $debug
30
- STDERR.printf "%-11s %-7s #{@marker.inspect} \n #{' '*11} state = %-8s str = %-20s buffer = %-20s cdata = %-20s\n",
31
- where, self.class, @state, @str.inspect, @buffer.inspect, @cdata.inspect
32
- end
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
33
24
  end
34
25
 
35
- def front
36
- @str[0]
26
+ def format_line(line)
27
+ return "" if line.nil?
28
+ format(line.chomp)
37
29
  end
38
30
 
39
- def grab(n=1)
40
- char = @str.slice!(0..(n-1)) # grab n chars
41
- char
31
+ def format_multiple(lines)
32
+ lines.map { |line| format_line(line) }
42
33
  end
43
34
 
44
- def grab_terminator
45
- @state = :LOOPING
46
- # goes onto buffer by default
47
- # Don't? what if searching for space_marker?
48
- # @buffer << grab
35
+ # Convenience methods for common formatting patterns
36
+ def bold(text)
37
+ "<b>#{text}</b>"
49
38
  end
50
39
 
51
- def eol?
52
- @str.empty?
40
+ def italic(text)
41
+ "<i>#{text}</i>"
53
42
  end
54
43
 
55
- def space?
56
- front == " "
44
+ def code(text)
45
+ "<tt>#{text}</tt>"
57
46
  end
58
47
 
59
- def escape?
60
- front == "\\"
48
+ def strike(text)
49
+ "<strike>#{text}</strike>"
61
50
  end
62
51
 
63
- def terminated?
64
- space? # Will be overridden except in Single
52
+ def link(text, url)
53
+ "<a href='#{url}'>#{text}</a>"
65
54
  end
66
55
 
67
- def marker?
68
- @str.start_with?(@marker)
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
69
67
  end
70
68
 
71
- def space_marker?
72
- @str.start_with?(" " + @marker)
73
- end
69
+ private
74
70
 
75
- def wrap(text)
76
- if text.empty?
77
- result = @marker
78
- result = "" if @marker[1] == "["
79
- return result
80
- end
81
- "<#{@tag}>#{text}</#{@tag}>"
82
- end
83
-
84
- def initial
85
- n = @marker.length
86
- case
87
- when escape?
88
- grab # backslash
89
- @buffer << grab # char
90
- when space_marker?
91
- @buffer << grab # append the space
92
- grab(n) # eat the marker
93
- @state = :CDATA
94
- when marker?
95
- grab(n) # Eat the marker
96
- @state = :CDATA
97
- when eol?
98
- @state = :FINAL
99
- else
100
- @state = :BUFFER
101
- end
71
+ def mark_escaped_characters(str)
72
+ # Replace escaped characters with a null byte marker (safe for internal use)
73
+ str.gsub(/\\([*_`~])/, "\u0000\\1")
102
74
  end
103
75
 
104
- def buffer
105
- @buffer << grab
106
- @state = :LOOPING
76
+ def unmark_escaped_characters(str)
77
+ # Restore escaped characters
78
+ str.gsub(/\u0000([*_`~])/, '\1')
107
79
  end
108
80
 
109
- def cdata
110
- case
111
- when eol?
112
- if @cdata.empty?
113
- @buffer << @marker unless @marker[1] == "["
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
114
87
  else
115
- @buffer << wrap(@cdata)
88
+ "<b>#{$1}</b>"
116
89
  end
117
- @state = :FINAL
118
- when terminated?
119
- @buffer << wrap(@cdata)
120
- grab_terminator # "*a *b" case???
121
- @cdata = ""
122
- @state = :LOOPING
123
- else
124
- @cdata << grab
125
- @state = :CDATA
126
90
  end
127
91
  end
128
92
 
129
- def looping
130
- n = @marker.length
131
- case
132
- when escape?
133
- grab # backslash
134
- @buffer << grab # char
135
- when space_marker?
136
- @buffer << grab # append the space
137
- grab(n) # eat the marker
138
- @state = :CDATA
139
- when eol?
140
- @state = :FINAL
141
- else # includes marker not preceded by space!
142
- @buffer << grab
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
143
116
  end
144
- end
145
-
146
- def handle
147
- loop do
148
- break if @state == :FINAL
149
- meth = @state.downcase
150
- send(meth)
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
151
  end
152
- return @buffer
153
- end
154
-
155
- def self.process(str)
156
- bold = self.new(str, "*", "b")
157
- sb = bold.handle
158
- # return sb
159
- ital = self.new(sb, "_", "i")
160
- si = ital.handle
161
- code = self.new(si, "`", "tt")
162
- sc = code.handle
163
- stri = self.new(sc, "~", "strike")
164
- si = stri.handle
165
- si
166
- end
167
- end
168
-
169
- class Livetext::Formatter::Single < Livetext::Formatter::Delimited
170
- # Yeah, this one is that simple
171
- end
172
-
173
- class Livetext::Formatter::Double < Livetext::Formatter::Delimited
174
- def initialize(str, sigil, tag) # Double
175
- super
176
- # Convention: marker is "**", sigil is "*"
177
- @marker = sigil + sigil
178
- end
179
-
180
- def terminated?
181
- terms = [" ", ".", ","]
182
- terms.include?(front)
183
152
  end
184
- end
185
153
 
186
- class Livetext::Formatter::Bracketed < Livetext::Formatter::Delimited
187
- def initialize(str, sigil, tag) # Bracketed
188
- super
189
- # Convention: marker is "*[", sigil is "*"
190
- @marker = sigil + "["
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
191
164
  end
192
165
 
193
- def terminated?
194
- front == "]" || eol?
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
195
176
  end
196
177
 
197
- def grab_terminator
198
- @state = :LOOPING
199
- grab
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
200
188
  end
201
189
  end
202
-