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.
- checksums.yaml +4 -4
- data/lib/livetext/ast/show_ast_clean.rb +10 -0
- data/lib/livetext/ast/show_ast_result.rb +60 -0
- data/lib/livetext/ast/show_raw_arrays.rb +13 -0
- data/lib/livetext/ast.rb +464 -0
- data/lib/livetext/ast_to_html.rb +32 -0
- data/lib/livetext/core.rb +6 -4
- data/lib/livetext/errors.rb +1 -0
- data/lib/livetext/functions.rb +26 -0
- data/lib/livetext/handler/mixin.rb +28 -0
- data/lib/livetext/helpers.rb +19 -20
- data/lib/livetext/standard.rb +2 -2
- data/lib/livetext/userapi.rb +20 -1
- data/lib/livetext/variable_manager.rb +14 -1
- data/lib/livetext/variables.rb +5 -1
- data/lib/livetext/version.rb +1 -1
- data/plugin/booktool.rb +6 -6
- data/plugin/lt3scriptor.rb +914 -0
- data/plugin/mixin_functions_class.rb +33 -0
- data/test/snapshots/error_missing_end/match-error.txt +1 -1
- data/test/snapshots/mixin_functions_class/expected-error.txt +0 -0
- data/test/snapshots/mixin_functions_class/expected-output.txt +20 -0
- data/test/snapshots/mixin_functions_class/match-error.txt +1 -0
- data/test/snapshots/mixin_functions_class/mixin_functions_class.rb +33 -0
- data/test/snapshots/mixin_functions_class/source.lt3 +17 -0
- data/test/snapshots/system_info/match-output.txt +18 -0
- data/test/unit/all.rb +3 -0
- data/test/unit/ast.rb +90 -0
- data/test/unit/ast_directives.rb +104 -0
- data/test/unit/ast_variables.rb +71 -0
- data/test/unit/core_methods.rb +180 -0
- data/test/unit/formatter_component.rb +1 -1
- data/test/unit/mixin_functions_class.rb +131 -0
- data/test/unit/stringparser.rb +14 -32
- metadata +19 -5
- data/imports/markdown.rb +0 -44
- data/plugin/markdown.rb +0 -43
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 03b835ce30fd58d0d0bdca2902661242763e9ea1c9cc71bb49bad16543b9f2f0
|
4
|
+
data.tar.gz: 36e4bf16acf045e4960ba5e5238c33561f640c63ae80be628805b15ccdefc00d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/livetext/ast.rb
ADDED
@@ -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.
|
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|
|
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?
|
data/lib/livetext/errors.rb
CHANGED
@@ -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
|
|
data/lib/livetext/functions.rb
CHANGED
@@ -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
|