livetext 0.9.52 → 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 (52) 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 +21 -21
  5. data/lib/livetext/formatter.rb +70 -200
  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/snapshots/complex_body/expected-error.txt +0 -0
  19. data/test/snapshots/complex_body/expected-output.txt +8 -0
  20. data/test/snapshots/complex_body/source.lt3 +19 -0
  21. data/test/snapshots/debug_command/expected-error.txt +0 -0
  22. data/test/snapshots/debug_command/expected-output.txt +1 -0
  23. data/test/snapshots/debug_command/source.lt3 +3 -0
  24. data/test/snapshots/def_parameters/expected-error.txt +0 -0
  25. data/test/snapshots/def_parameters/expected-output.txt +21 -0
  26. data/test/snapshots/def_parameters/source.lt3 +44 -0
  27. data/test/snapshots/functions_reflection/expected-error.txt +0 -0
  28. data/test/snapshots/functions_reflection/expected-output.txt +27 -0
  29. data/test/snapshots/functions_reflection/source.lt3 +5 -0
  30. data/test/snapshots/multiple_functions/expected-error.txt +0 -0
  31. data/test/snapshots/multiple_functions/expected-output.txt +5 -0
  32. data/test/snapshots/multiple_functions/source.lt3 +16 -0
  33. data/test/snapshots/nested_includes/expected-error.txt +0 -0
  34. data/test/snapshots/nested_includes/expected-output.txt +68 -0
  35. data/test/snapshots/nested_includes/level2.inc +34 -0
  36. data/test/snapshots/nested_includes/level3.inc +20 -0
  37. data/test/snapshots/nested_includes/source.lt3 +49 -0
  38. data/test/snapshots/parameter_handling/expected-error.txt +0 -0
  39. data/test/snapshots/parameter_handling/expected-output.txt +7 -0
  40. data/test/snapshots/parameter_handling/source.lt3 +10 -0
  41. data/test/snapshots/subset.txt +1 -0
  42. data/test/snapshots/system_info/expected-error.txt +0 -0
  43. data/test/snapshots/system_info/expected-output.txt +18 -0
  44. data/test/snapshots/system_info/source.lt3 +16 -0
  45. data/test/unit/all.rb +4 -0
  46. data/test/unit/core_methods.rb +137 -0
  47. data/test/unit/formatter.rb +84 -0
  48. data/test/unit/formatter_component.rb +84 -0
  49. data/test/unit/function_registry.rb +132 -0
  50. data/test/unit/variable_manager.rb +71 -0
  51. metadata +36 -3
  52. data/lib/livetext/processor.rb +0 -88
@@ -0,0 +1,21 @@
1
+ Testing no parameters:
2
+ No parameters method called
3
+ <p>
4
+
5
+ Testing with args and data:
6
+ Args: ["arg1", "arg2"], Data: "arg1 arg2"
7
+ This is the data line
8
+ <p>
9
+
10
+ Testing with body:
11
+ Args: ["arg1", "arg2"], Data: "arg1 arg2"
12
+ Body has 3 lines:
13
+ 1: This is line 1 of the body
14
+ 2: This is line 2 of the body
15
+ 3: This is line 3 of the body
16
+ Testing with raw body:
17
+ Args: ["arg1", "arg2"], Data: "arg1 arg2"
18
+ Raw body has 3 lines:
19
+ 1: This is line 1 of the raw body
20
+ 2: This is line 2 of the raw body
21
+ 3: This is line 3 of the raw body
@@ -0,0 +1,44 @@
1
+ .def no_params
2
+ api.out "No parameters method called"
3
+ .end
4
+
5
+ .def with_args args
6
+ api.out "Args: #{args.inspect}, Data: #{data.inspect}"
7
+ .end
8
+
9
+ .def with_body body
10
+ api.out "Args: #{args.inspect}, Data: #{data.inspect}"
11
+ api.out "Body has #{body.length} lines:"
12
+ body.each_with_index do |line, i|
13
+ api.out " #{i+1}: #{line}"
14
+ end
15
+ .end
16
+
17
+ .def with_body_raw body raw
18
+ api.out "Args: #{args.inspect}, Data: #{data.inspect}"
19
+ api.out "Raw body has #{body.length} lines:"
20
+ body.each_with_index do |line, i|
21
+ api.out " #{i+1}: #{line}"
22
+ end
23
+ .end
24
+
25
+ Testing no parameters:
26
+ .no_params
27
+
28
+ Testing with args and data:
29
+ .with_args arg1 arg2
30
+ This is the data line
31
+
32
+ Testing with body:
33
+ .with_body arg1 arg2
34
+ This is line 1 of the body
35
+ This is line 2 of the body
36
+ This is line 3 of the body
37
+ .end
38
+
39
+ Testing with raw body:
40
+ .with_body_raw arg1 arg2
41
+ This is line 1 of the raw body
42
+ This is line 2 of the raw body
43
+ This is line 3 of the raw body
44
+ .end
@@ -0,0 +1,27 @@
1
+ <h3>Available Functions</h3>
2
+ <ul>
3
+ <li><strong>$$br</strong> - builtin</li>
4
+ <li><strong>$$date</strong> - builtin</li>
5
+ <li><strong>$$day</strong> - builtin</li>
6
+ <li><strong>$$days_ago</strong> - builtin</li>
7
+ <li><strong>$$days_from_now</strong> - builtin</li>
8
+ <li><strong>$$format_date</strong> - builtin</li>
9
+ <li><strong>$$hostname</strong> - builtin</li>
10
+ <li><strong>$$hour</strong> - builtin</li>
11
+ <li><strong>$$isqrt</strong> - builtin</li>
12
+ <li><strong>$$link</strong> - builtin</li>
13
+ <li><strong>$$livetext_version</strong> - builtin</li>
14
+ <li><strong>$$minute</strong> - builtin</li>
15
+ <li><strong>$$month</strong> - builtin</li>
16
+ <li><strong>$$platform</strong> - builtin</li>
17
+ <li><strong>$$pwd</strong> - builtin</li>
18
+ <li><strong>$$rand</strong> - builtin</li>
19
+ <li><strong>$$reverse</strong> - builtin</li>
20
+ <li><strong>$$ruby_version</strong> - builtin</li>
21
+ <li><strong>$$second</strong> - builtin</li>
22
+ <li><strong>$$testfunc</strong> - inline (unknown)</li>
23
+ <li><strong>$$time</strong> - builtin</li>
24
+ <li><strong>$$week</strong> - builtin</li>
25
+ <li><strong>$$weekday</strong> - builtin</li>
26
+ <li><strong>$$year</strong> - builtin</li>
27
+ </ul>
@@ -0,0 +1,5 @@
1
+ .func testfunc
2
+ "Test function: #{param}"
3
+ .end
4
+
5
+ .functions
@@ -0,0 +1,5 @@
1
+ Testing multiple user functions in the same document...
2
+ <p>
3
+
4
+ Calling all functions:
5
+ one
@@ -0,0 +1,16 @@
1
+ Testing multiple user functions in the same document...
2
+
3
+ .func func1
4
+ 'one'
5
+ .end
6
+
7
+ .func func2
8
+ 'two'
9
+ .end
10
+
11
+ .func func3
12
+ 'three'
13
+ .end
14
+
15
+ Calling all functions:
16
+ $$func1[] and $$func2[] and $$func3[]
File without changes
@@ -0,0 +1,68 @@
1
+ # Main file - Level 1
2
+ # This file includes level2 and tests all variables and functions from all levels
3
+ <p>
4
+
5
+ # Include level2 (which includes level3)
6
+ # Level 2 - Middle level
7
+ # This file includes level3 and defines its own variables and functions
8
+ <p>
9
+
10
+ # Level 3 - Deepest level
11
+ # This file defines variables and functions that should be accessible to all higher levels
12
+ <p>
13
+
14
+ This is content from level 3.
15
+ The deep variable is: I am from level 3
16
+ The deep number is: 42
17
+ This is content from level 2.
18
+ The middle variable is: I am from level 2
19
+ The middle number is: 100
20
+ <p>
21
+
22
+ # Test using deep level variables and functions
23
+ Deep variable from level 2: I am from level 3
24
+ Deep function call: Deep function called with: from level 2
25
+ Deep math: 42 = 42
26
+ <p>
27
+
28
+ # Test our own variables and functions
29
+ Middle function call: Middle function called with: from level 2
30
+ Combined variables: Deep: I am from level 3, Middle: I am from level 2
31
+ <p>
32
+
33
+ # Test deep method from level 3
34
+ Deep method called from level 3
35
+ # Test all variables from all levels
36
+ Main variable: I am from main level
37
+ Middle variable: I am from level 2
38
+ Deep variable: I am from level 3
39
+ <p>
40
+
41
+ # Test all functions from all levels
42
+ Main function: Main function called with: from main
43
+ Middle function: Middle function called with: from main
44
+ Deep function: Deep function called with: from main
45
+ <p>
46
+
47
+ # Test math functions
48
+ Deep math: 20 = 20
49
+ Main number: 999
50
+ Middle number: 100
51
+ Deep number: 42
52
+ <p>
53
+
54
+ # Test combined functions
55
+ Combine vars: Deep: I am from level 3, Middle: I am from level 2
56
+ Test all levels: Main: I am from main level, Middle: I am from level 2, Deep: I am from level 3
57
+ <p>
58
+
59
+ # Test methods from all levels
60
+ Main method called from main level
61
+ Middle method called from level 2
62
+ Deep method called from level 3
63
+ <p>
64
+
65
+ # Test that we can still access everything after the include
66
+ Final test - deep function: Deep function called with: final call
67
+ Final test - middle function: Middle function called with: final call
68
+ Final test - main function: Main function called with: final call
@@ -0,0 +1,34 @@
1
+ # Level 2 - Middle level
2
+ # This file includes level3 and defines its own variables and functions
3
+
4
+ .include level3.inc
5
+
6
+ .set middle_var="I am from level 2", middle_number=100
7
+
8
+ .func middle_function
9
+ "Middle function called with: " + param
10
+ .end
11
+
12
+ .func combine_vars
13
+ "Deep: " + "I am from level 3" + ", Middle: " + "I am from level 2"
14
+ .end
15
+
16
+ This is content from level 2.
17
+ The middle variable is: $middle_var
18
+ The middle number is: $middle_number
19
+
20
+ # Test using deep level variables and functions
21
+ Deep variable from level 2: $deep_var
22
+ Deep function call: $$deep_function[from level 2]
23
+ Deep math: $$deep_math[21] = 42
24
+
25
+ # Test our own variables and functions
26
+ Middle function call: $$middle_function[from level 2]
27
+ Combined variables: $$combine_vars[]
28
+
29
+ .def middle_method
30
+ api.out "Middle method called from level 2"
31
+ .end
32
+
33
+ # Test deep method from level 3
34
+ .deep_method
@@ -0,0 +1,20 @@
1
+ # Level 3 - Deepest level
2
+ # This file defines variables and functions that should be accessible to all higher levels
3
+
4
+ .set deep_var="I am from level 3", deep_number=42
5
+
6
+ .func deep_function
7
+ "Deep function called with: " + param
8
+ .end
9
+
10
+ .func deep_math
11
+ (param.to_i * 2).to_s
12
+ .end
13
+
14
+ .def deep_method
15
+ api.out "Deep method called from level 3"
16
+ .end
17
+
18
+ This is content from level 3.
19
+ The deep variable is: $deep_var
20
+ The deep number is: $deep_number
@@ -0,0 +1,49 @@
1
+ # Main file - Level 1
2
+ # This file includes level2 and tests all variables and functions from all levels
3
+
4
+ .set main_var="I am from main level", main_number=999
5
+
6
+ .func main_function
7
+ "Main function called with: " + param
8
+ .end
9
+
10
+ .func test_all_levels
11
+ "Main: " + "I am from main level" + ", Middle: " + "I am from level 2" + ", Deep: " + "I am from level 3"
12
+ .end
13
+
14
+ # Include level2 (which includes level3)
15
+ .include level2.inc
16
+
17
+ # Test all variables from all levels
18
+ Main variable: $main_var
19
+ Middle variable: $middle_var
20
+ Deep variable: $deep_var
21
+
22
+ # Test all functions from all levels
23
+ Main function: $$main_function[from main]
24
+ Middle function: $$middle_function[from main]
25
+ Deep function: $$deep_function[from main]
26
+
27
+ # Test math functions
28
+ Deep math: $$deep_math[10] = 20
29
+ Main number: $main_number
30
+ Middle number: $middle_number
31
+ Deep number: $deep_number
32
+
33
+ # Test combined functions
34
+ Combine vars: $$combine_vars[]
35
+ Test all levels: $$test_all_levels[]
36
+
37
+ .def main_method
38
+ api.out "Main method called from main level"
39
+ .end
40
+
41
+ # Test methods from all levels
42
+ .main_method
43
+ .middle_method
44
+ .deep_method
45
+
46
+ # Test that we can still access everything after the include
47
+ Final test - deep function: $$deep_function[final call]
48
+ Final test - middle function: $$middle_function[final call]
49
+ Final test - main function: $$main_function[final call]
@@ -0,0 +1,7 @@
1
+ Testing function parameter handling...
2
+ <p>
3
+
4
+ Testing with no parameter: nil
5
+ Testing with empty parameter: nil
6
+ Testing with string parameter: hello
7
+ Testing with number parameter: 42
@@ -0,0 +1,10 @@
1
+ Testing function parameter handling...
2
+
3
+ .func testparam
4
+ param.empty? ? 'nil' : param
5
+ .end
6
+
7
+ Testing with no parameter: $$testparam[]
8
+ Testing with empty parameter: $$testparam[]
9
+ Testing with string parameter: $$testparam[hello]
10
+ Testing with number parameter: $$testparam[42]
@@ -23,6 +23,7 @@ h This file specifies which snapshots will/won't be run.
23
23
 
24
24
  # import/include/mixin, others...
25
25
 
26
+
26
27
  error_no_such_inc # Output BEFORE error doesn't get passed through ("leading" output)
27
28
  error_no_such_copy # ^ Same behavior as error_no_such_inc
28
29
  error_no_such_mixin # ^ Same behavior as error_missing_end
File without changes
@@ -0,0 +1,18 @@
1
+ System Info Variables:
2
+ Hostname: HAL9000
3
+ Platform: universal.x86_64-darwin22
4
+ Ruby Version: 2.6.10
5
+ Livetext Version: 0.9.53
6
+ <p>
7
+
8
+ System Info Functions:
9
+ Hostname: HAL9000
10
+ Platform: universal.x86_64-darwin22
11
+ Ruby Version: 2.6.10
12
+ Livetext Version: 0.9.53
13
+ <p>
14
+
15
+ Date Formatting Functions:
16
+ Default: 2025-08-07
17
+ Days ago: 2025-07-31
18
+ Days from now: 2025-08-14
@@ -0,0 +1,16 @@
1
+ System Info Variables:
2
+ Hostname: $Hostname
3
+ Platform: $Platform
4
+ Ruby Version: $RubyVersion
5
+ Livetext Version: $LivetextVersion
6
+
7
+ System Info Functions:
8
+ Hostname: $$hostname
9
+ Platform: $$platform
10
+ Ruby Version: $$ruby_version
11
+ Livetext Version: $$livetext_version
12
+
13
+ Date Formatting Functions:
14
+ Default: $$format_date
15
+ Days ago: $$days_ago[7]
16
+ Days from now: $$days_from_now[7]
data/test/unit/all.rb CHANGED
@@ -7,5 +7,9 @@ require_relative 'single'
7
7
  require_relative 'double'
8
8
  require_relative 'bracketed'
9
9
  require_relative 'variables'
10
+ require_relative 'variable_manager'
10
11
  require_relative 'functions'
12
+ require_relative 'function_registry'
13
+ require_relative 'formatter'
14
+ require_relative 'core_methods'
11
15
 
@@ -0,0 +1,137 @@
1
+ require 'minitest/autorun'
2
+ require_relative '../../lib/livetext'
3
+
4
+ class TestingLivetextCoreMethods < Minitest::Test
5
+ def setup
6
+ @live = Livetext.new
7
+ end
8
+
9
+ def test_customize_class_method
10
+ # Test the class-level customize method
11
+ obj = Livetext.customize(
12
+ mix: [],
13
+ call: ['.nopara'],
14
+ vars: { 'test_var' => 'test_value' }
15
+ )
16
+
17
+ assert_instance_of(Livetext, obj)
18
+ # The customize method should set up the instance with mixins, commands, and vars
19
+ end
20
+
21
+ def test_customize_instance_method
22
+ # Test the instance-level customize method
23
+ result = @live.customize(
24
+ mix: [],
25
+ call: ['.nopara'],
26
+ vars: { 'test_var' => 'test_value' }
27
+ )
28
+
29
+ assert_equal(@live, result) # Should return self
30
+ end
31
+
32
+ def test_process_text
33
+ # Test processing text directly
34
+ text = "Hello world\nThis is a test\n.h1 Title"
35
+ body, vars = @live.process(text: text)
36
+
37
+ assert_includes(body, "Hello world")
38
+ assert_includes(body, "This is a test")
39
+ # Should process the .h1 command
40
+ assert_instance_of(Hash, vars)
41
+ end
42
+
43
+ def test_process_file
44
+ # Test processing a file
45
+ # Create a temporary test file
46
+ require 'tempfile'
47
+ temp_file = Tempfile.new(['test', '.lt3'])
48
+ temp_file.write("Hello from file\n.h1 File Title")
49
+ temp_file.close
50
+
51
+ body, vars = @live.process(file: temp_file.path)
52
+
53
+ assert_includes(body, "Hello from file")
54
+ # Should process the .h1 command
55
+ assert_instance_of(Hash, vars)
56
+
57
+ temp_file.unlink
58
+ end
59
+
60
+ def test_process_with_file
61
+ # Test process method with file parameter
62
+ require 'tempfile'
63
+ temp_file = Tempfile.new(['test', '.lt3'])
64
+ temp_file.write("Hello from process file\n.h1 Process Title")
65
+ temp_file.close
66
+
67
+ body, vars = @live.process(file: temp_file.path)
68
+
69
+ assert_includes(body, "Hello from process file")
70
+ # Should process the .h1 command
71
+ assert_instance_of(Hash, vars)
72
+
73
+ temp_file.unlink
74
+ end
75
+
76
+ def test_process_with_text
77
+ # Test process method with text parameter
78
+ text = "Hello from process text\n.h1 Process Text Title"
79
+ body, vars = @live.process(text: text)
80
+
81
+ assert_includes(body, "Hello from process text")
82
+ # Should process the .h1 command
83
+ assert_instance_of(Hash, vars)
84
+ end
85
+
86
+ def test_process_with_vars
87
+ # Test process method with variables
88
+ text = "Hello $name\n.h1 $title"
89
+ body, vars = @live.process(text: text, vars: { 'name' => 'World', 'title' => 'Test Title' })
90
+
91
+ assert_includes(body, "Hello World")
92
+ assert_includes(body, "Test Title")
93
+ assert_equal('World', vars[:name])
94
+ assert_equal('Test Title', vars[:title])
95
+ end
96
+
97
+ def test_process_error_handling
98
+ # Test process method error cases
99
+ assert_raises(RuntimeError) { @live.process } # No file or text
100
+ assert_raises(RuntimeError) { @live.process(file: 'nonexistent.lt3', text: 'some text') } # Both file and text
101
+ end
102
+
103
+ def test_backward_compatibility
104
+ # Test that old methods still work
105
+ text = "Hello from old method\n.h1 Old Title"
106
+
107
+ # Test transform
108
+ result1 = @live.transform(text)
109
+ assert_includes(result1, "Hello from old method")
110
+
111
+ # Test xform with text
112
+ result2 = @live.xform(text: text)
113
+ assert_includes(result2, "Hello from old method")
114
+
115
+ # Test xform_file
116
+ require 'tempfile'
117
+ temp_file = Tempfile.new(['test', '.lt3'])
118
+ temp_file.write("Hello from old file method\n.h1 Old File Title")
119
+ temp_file.close
120
+
121
+ result3 = @live.xform_file(temp_file.path)
122
+ assert_includes(result3, "Hello from old file method")
123
+
124
+ temp_file.unlink
125
+ end
126
+
127
+ def test_process_returns_array
128
+ # Test that process returns [body, vars] array
129
+ text = "Hello $name\n.set title=\"Test Title\"\n.h1 $title"
130
+ body, vars = @live.process(text: text, vars: { 'name' => 'World' })
131
+
132
+ assert_includes(body, "Hello World")
133
+ assert_includes(body, "Test Title")
134
+ assert_equal('World', vars[:name])
135
+ assert_equal('Test Title', vars[:title])
136
+ end
137
+ end
@@ -0,0 +1,84 @@
1
+ require 'minitest/autorun'
2
+
3
+ require_relative '../../lib/livetext'
4
+
5
+ class TestingLivetextFormatter < Minitest::Test
6
+ def setup
7
+ @live = Livetext.new
8
+ end
9
+
10
+ def test_formatter_component_initialization
11
+ # Test that Formatter is properly initialized
12
+ assert(@live.formatter, "Formatter should be initialized")
13
+ assert_instance_of(Livetext::Formatter, @live.formatter)
14
+ end
15
+
16
+ def test_basic_formatting
17
+ # Test basic text formatting
18
+ assert_equal("<b>bold</b>", @live.formatter.format("*bold"))
19
+ assert_equal("<i>italic</i>", @live.formatter.format("_italic"))
20
+ assert_equal("<tt>code</tt>", @live.formatter.format("`code"))
21
+ assert_equal("<strike>strike</strike>", @live.formatter.format("~strike"))
22
+ end
23
+
24
+ def test_double_markers
25
+ # Test double markers
26
+ assert_equal("<b>bold</b>", @live.formatter.format("**bold"))
27
+ assert_equal("**", @live.formatter.format("**")) # standalone
28
+ assert_equal(" ** ", @live.formatter.format(" ** ")) # surrounded by spaces
29
+ end
30
+
31
+ def test_bracketed_markers
32
+ # Test bracketed markers
33
+ assert_equal("<b>content</b>", @live.formatter.format("*[content]"))
34
+ assert_equal("<i>content</i>", @live.formatter.format("_[content]"))
35
+ assert_equal("<tt>content</tt>", @live.formatter.format("`[content]"))
36
+ assert_equal("", @live.formatter.format("*[]")) # empty brackets disappear
37
+ end
38
+
39
+ def test_escaped_markers
40
+ # Test escaped markers
41
+ assert_equal("*literal", @live.formatter.format("\\*literal"))
42
+ assert_equal("_literal", @live.formatter.format("\\_literal"))
43
+ assert_equal("`literal", @live.formatter.format("\\`literal"))
44
+ assert_equal("~literal", @live.formatter.format("\\~literal"))
45
+ end
46
+
47
+ def test_format_line
48
+ # Test format_line method
49
+ assert_equal("<b>bold</b>", @live.formatter.format_line("*bold\n"))
50
+ assert_equal("", @live.formatter.format_line(nil))
51
+ assert_equal("", @live.formatter.format_line(""))
52
+ end
53
+
54
+ def test_format_multiple
55
+ # Test format_multiple method
56
+ lines = ["*bold\n", "_italic\n", "`code\n"]
57
+ expected = ["<b>bold</b>", "<i>italic</i>", "<tt>code</tt>"]
58
+ assert_equal(expected, @live.formatter.format_multiple(lines))
59
+ end
60
+
61
+ def test_convenience_methods
62
+ # Test convenience methods
63
+ assert_equal("<b>text</b>", @live.formatter.bold("text"))
64
+ assert_equal("<i>text</i>", @live.formatter.italic("text"))
65
+ assert_equal("<tt>text</tt>", @live.formatter.code("text"))
66
+ assert_equal("<strike>text</strike>", @live.formatter.strike("text"))
67
+ assert_equal("<a href='url'>text</a>", @live.formatter.link("text", "url"))
68
+ end
69
+
70
+ def test_escape_html
71
+ # Test HTML escaping
72
+ assert_equal("&lt;tag&gt;", @live.formatter.escape_html("<tag>"))
73
+ assert_equal("&amp;", @live.formatter.escape_html("&"))
74
+ assert_equal("&quot;text&quot;", @live.formatter.escape_html('"text"'))
75
+ assert_equal("&#39;text&#39;", @live.formatter.escape_html("'text'"))
76
+ end
77
+
78
+ def test_edge_cases
79
+ # Test edge cases
80
+ assert_equal("", @live.formatter.format(nil))
81
+ assert_equal("", @live.formatter.format(""))
82
+ assert_equal("plain text", @live.formatter.format("plain text"))
83
+ end
84
+ end