livetext 0.9.25 → 0.9.26

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/imports/bookish.rb +1 -2
  3. data/lib/livetext/errors.rb +3 -0
  4. data/lib/livetext/formatline.rb +102 -15
  5. data/lib/livetext/funcall.rb +86 -2
  6. data/lib/livetext/global_helpers.rb +5 -0
  7. data/lib/livetext/handler/import.rb +2 -6
  8. data/lib/livetext/handler/mixin.rb +2 -6
  9. data/lib/livetext/helpers.rb +9 -11
  10. data/lib/livetext/lineparser.rb +441 -0
  11. data/lib/livetext/more.rb +158 -0
  12. data/lib/livetext/processor.rb +3 -1
  13. data/lib/livetext/skeleton.rb +5 -0
  14. data/lib/livetext/standard.rb +12 -8
  15. data/lib/livetext/userapi.rb +27 -10
  16. data/lib/livetext/version.rb +1 -1
  17. data/lib/livetext.rb +3 -152
  18. data/test/snapshots/basic_formatting/actual-error.txt +0 -0
  19. data/test/snapshots/basic_formatting/actual-output.txt +13 -0
  20. data/test/snapshots/basic_formatting/err-sdiff.txt +1 -0
  21. data/test/snapshots/basic_formatting/out-sdiff.txt +14 -0
  22. data/test/snapshots/error_invalid_name/foo +5 -0
  23. data/test/snapshots/import_bookish/expected-output.txt +4 -4
  24. data/test/snapshots/more_functions/actual-error.txt +0 -0
  25. data/test/snapshots/more_functions/actual-output.txt +37 -0
  26. data/test/snapshots/more_functions/err-sdiff.txt +1 -0
  27. data/test/snapshots/more_functions/expected-output.txt +1 -1
  28. data/test/snapshots/more_functions/out-sdiff.txt +38 -0
  29. data/test/snapshots/more_functions/source.lt3 +1 -1
  30. data/test/snapshots/simple_vars/actual-error.txt +0 -0
  31. data/test/snapshots/simple_vars/actual-output.txt +6 -0
  32. data/test/snapshots/simple_vars/err-sdiff.txt +1 -0
  33. data/test/snapshots/simple_vars/out-sdiff.txt +7 -0
  34. data/test/snapshots/subset.txt +2 -0
  35. data/test/snapshots/var_into_func/actual-error.txt +0 -0
  36. data/test/snapshots/var_into_func/actual-output.txt +16 -0
  37. data/test/snapshots/var_into_func/err-sdiff.txt +1 -0
  38. data/test/snapshots/var_into_func/expected-error.txt +0 -0
  39. data/test/snapshots/var_into_func/expected-output.txt +16 -0
  40. data/test/snapshots/var_into_func/out-sdiff.txt +17 -0
  41. data/test/snapshots/var_into_func/source.lt3 +16 -0
  42. data/test/unit/all.rb +3 -1
  43. data/test/unit/formatline.rb +143 -274
  44. data/test/unit/lineparser.rb +650 -0
  45. data/test/unit/parser/set.rb +13 -12
  46. data/test/unit/tokenizer.rb +534 -0
  47. metadata +26 -5
  48. data/test/snapshots/error_inc_line_num/OUT +0 -17
  49. data/test/snapshots/error_no_such_copy/duh +0 -26
  50. data/test/snapshots/error_no_such_copy/mystery.txt +0 -36
@@ -0,0 +1,441 @@
1
+
2
+ require_relative 'parsing'
3
+ require_relative 'funcall'
4
+
5
+ # Class LineParser handles the parsing of comments, dot commands, and
6
+ # simple formatting characters, as well as variables and functions.
7
+
8
+ class Livetext::LineParser < StringParser
9
+ include Livetext::ParsingConstants
10
+ include Livetext::LineParser::FunCall
11
+
12
+ FMTS = %w[* _ ~ `]
13
+
14
+ attr_reader :out
15
+ attr_reader :tokenlist
16
+
17
+ def initialize(line)
18
+ super
19
+ @token = Null.dup
20
+ @tokenlist = []
21
+ @live = Livetext.new
22
+ end
23
+
24
+ def self.api
25
+ Livetext.new.main.api
26
+ end
27
+
28
+ def api
29
+ @live.main.api
30
+ end
31
+
32
+ def self.parse!(line)
33
+ return nil if line.nil?
34
+ line.chomp!
35
+ x = self.new(line)
36
+ api.tty "\n-- string: #{line.inspect}" if $testme
37
+ t = x.tokenize
38
+ api.tty "-- Tokens: #{t.inspect}" if $testme
39
+ result = x.evaluate
40
+ api.tty "-- result: #{result.inspect}" if $testme
41
+ result
42
+ end
43
+
44
+ def parse_formatting
45
+ loop do
46
+ case peek
47
+ when Escape; grab; add peek; grab
48
+ when "*", "_", "`", "~"
49
+ marker peek
50
+ add peek
51
+ when LF
52
+ break if eos?
53
+ when nil
54
+ break
55
+ else
56
+ add peek
57
+ end
58
+ grab
59
+ end
60
+ add_token(:str)
61
+ @tokenlist
62
+ end
63
+
64
+ def self.parse_formatting(str)
65
+ fmt = self.new(str)
66
+ loop do
67
+ case fmt.peek
68
+ when Escape; fmt.grab; fmt.add fmt.peek; fmt.grab
69
+ when "*", "_", "`", "~"
70
+ fmt.marker fmt.peek
71
+ fmt.add fmt.peek
72
+ when LF
73
+ break if fmt.eos?
74
+ when nil
75
+ break
76
+ else
77
+ fmt.add fmt.peek
78
+ end
79
+ fmt.grab
80
+ end
81
+ fmt.add_token(:str)
82
+ fmt.tokenlist
83
+ end
84
+
85
+ def self.parse_variables(str)
86
+ return nil if str.nil?
87
+ x = self.new(str.chomp)
88
+ char = x.peek
89
+ loop do
90
+ char = x.grab
91
+ break if char == LF || char == nil
92
+ x.escaped if char == Escape
93
+ x.dollar if char == "$" # Could be $$
94
+ x.add char
95
+ end
96
+ x.add_token(:str)
97
+ result = x.evaluate
98
+ result
99
+ end
100
+
101
+ def embed(sym, str)
102
+ pre, post = SimpleFormats[sym]
103
+ pre + str + post
104
+ end
105
+
106
+ #########
107
+
108
+ def handle_simple_formatting(ch)
109
+ # api.tty "ch = #{ch.inspect}"
110
+ marker ch
111
+ add ch
112
+ # api.tty "token = #{@token.inspect} #{@tokenlist.inspect}"
113
+ c2 = grab
114
+ # api.tty "c2 = #{c2.inspect}"
115
+ end
116
+
117
+ def grab_string
118
+ weird = ["$", nil] # [Escape, "$", nil]
119
+ ch = grab
120
+ add ch # api.tty "-- gs @token = #{@token.inspect}"
121
+ loop do
122
+ ch = peek # api.tty "gs1 ch = #{ch.inspect}"
123
+ break if weird.include?(ch)
124
+ break if FMTS.include?(ch) && (self.prev == " ")
125
+ break if eos? # ch = grab # advance pointer # api.tty "gs3 ch = #{ch.inspect}"
126
+ add grab
127
+ end # ch = grab # advance pointer # api.tty "-- gs4 ch = #{ch.inspect}"; sleep 0.01
128
+ add_token :str
129
+ end
130
+
131
+ def grab_token(ch)
132
+ finish = false
133
+ # api.tty "#{__method__}: ch = #{ch.inspect}"
134
+ case ch
135
+ when nil; finish = true # do nothing
136
+ when LF; finish = true # do nothing - break if eos?
137
+ when Escape; ch = self.escaped; add ch
138
+ when "$"; dollar
139
+ when *FMTS; handle_simple_formatting(ch)
140
+ else grab_string
141
+ end
142
+ # api.tty "#{__method__}: AFTER CASE: api.data = #{api.data.inspect}"
143
+ [ch, finish, @token, @tokenlist]
144
+ end
145
+
146
+ def tokenize
147
+ ch = peek
148
+ loop do
149
+ ch = peek
150
+ stuff = grab_token(ch)
151
+ ch, finish, t, tlist = *stuff
152
+ break if finish
153
+ end
154
+ # api.tty "tokenize: i = #{self.i}"
155
+ # api.tty "tokenize: token = #{@token.inspect} tokenlist = #{@tokenlist.inspect}"
156
+ @tokenlist
157
+ end
158
+
159
+ # def self.get_vars
160
+ # grab
161
+ # case peek
162
+ # when LF, " ", nil
163
+ # add "$"
164
+ # add_token :str
165
+ # when "$"; double_dollar
166
+ ## when "."; dollar_dot
167
+ # when /[A-Za-z]/
168
+ # add_token :str
169
+ # var = peek + grab_alpha_dot
170
+ # add_token(:var, var)
171
+ # else
172
+ # add "$" + peek
173
+ # add_token(:str)
174
+ # end
175
+ # end
176
+ #
177
+ # def self.parse_var_func # FIXME Hmm...
178
+ # loop do
179
+ # case peek
180
+ # when "$"
181
+ # dollar
182
+ # when LF
183
+ # break if eos?
184
+ # when nil
185
+ # break
186
+ # else
187
+ # add peek
188
+ # end
189
+ # grab
190
+ # end
191
+ # add_token(:str)
192
+ # @tokenlist
193
+ # end
194
+
195
+ def terminate?(terminators, ch)
196
+ if terminators.is_a? Regexp
197
+ terminators === ch
198
+ else
199
+ terminators.include?(ch)
200
+ end
201
+ end
202
+
203
+ def var_func_parse
204
+ char = self.peek
205
+ loop do
206
+ char = self.grab
207
+ break if char == LF || char == nil
208
+ self.escaped if char == Escape
209
+ self.dollar if char == "$" # Could be $$
210
+ self.add char
211
+ end
212
+ self.add_token(:str)
213
+ result = self.evaluate
214
+ result
215
+ end
216
+
217
+ def self.var_func_parse(str)
218
+ return nil if str.nil?
219
+ x = self.new(str.chomp)
220
+ char = x.peek
221
+ loop do
222
+ char = x.grab
223
+ break if char == LF || char == nil
224
+ x.escaped if char == Escape
225
+ x.dollar if char == "$" # Could be $$
226
+ x.add char
227
+ end
228
+ x.add_token(:str)
229
+ result = x.evaluate
230
+ result
231
+ end
232
+
233
+ def evaluate(tokens = @tokenlist)
234
+ @out = ""
235
+ return "" if tokens.empty?
236
+ gen = tokens.each
237
+ token = gen.next
238
+ loop do
239
+ break if token.nil?
240
+ sym, val = *token
241
+ case sym
242
+ when :str; eval_str(val)
243
+ when :var; eval_var(val)
244
+ when :func; eval_func(val, gen)
245
+ when *BITS; eval_bits(sym, val)
246
+ else
247
+ add_token :str
248
+ end
249
+ token = gen.next
250
+ end
251
+ @out
252
+ end
253
+
254
+ def add(str)
255
+ @token << str unless str.nil?
256
+ end
257
+
258
+ def add_token(kind, token = @token)
259
+ return if token.nil?
260
+ @tokenlist << [kind, token] unless token.empty?
261
+ @token = Null.dup
262
+ end
263
+
264
+ def grab_alpha
265
+ str = grab
266
+ loop do
267
+ break if eos?
268
+ break if terminate?(NoAlpha, peek)
269
+ str << grab
270
+ end
271
+ str
272
+ end
273
+
274
+ def grab_alpha_dot
275
+ str = grab # Null.dup
276
+ loop do
277
+ break if eos?
278
+ break if terminate?(NoAlphaDot, peek)
279
+ str << grab
280
+ end
281
+ str
282
+ end
283
+
284
+ def dollar
285
+ c1 = grab # $
286
+ c2 = grab # ...
287
+ case c2
288
+ when " "; add_token :str, "$ "
289
+ when LF, nil; add_token :str, "$"
290
+ when "$"; double_dollar
291
+ when "."; dollar_dot
292
+ when /[A-Za-z]/; add_token(:var, c2 + grab_alpha_dot)
293
+ else add_token(:str, "$" + c2)
294
+ end
295
+ end
296
+
297
+ def finish_token(str, kind)
298
+ add str
299
+ add_token :str
300
+ grab
301
+ end
302
+
303
+ def marker(char)
304
+ add_token :str
305
+ sym = Syms[char]
306
+ return if embedded?
307
+ grab
308
+ case peek
309
+ when Space; finish_token(char + " ", :str)
310
+ when LF, nil; finish_token(char, :str)
311
+ when char; double_marker(char)
312
+ when LBrack; long_marker(char)
313
+ else
314
+ str = peek + collect!(sym, Blank)
315
+ add str
316
+ add_token sym, str
317
+ grab
318
+ end
319
+ end
320
+
321
+ def double_marker(char)
322
+ sym = Syms[char]
323
+ kind = sym
324
+ case lookahead # first char after **
325
+ when Space, LF, nil
326
+ pre, post = SimpleFormats[sym]
327
+ add_token kind
328
+ else
329
+ str = collect!(sym, Punc)
330
+ add_token kind, str
331
+ grab
332
+ end
333
+ end
334
+
335
+ def long_marker(char)
336
+ sym = Syms[char]
337
+ grab # skip left bracket
338
+ kind = sym # "param_#{sym}".to_sym
339
+ arg = collect!(sym, Param, true)
340
+ add_token kind, arg
341
+ end
342
+
343
+ def collect_bracketed(sym, terminators)
344
+ str = Null.dup # next is not " ","*","["
345
+ grab # ZZZ
346
+ loop do
347
+ if peek == Escape
348
+ grab
349
+ str << grab
350
+ next
351
+ end
352
+ if terminate?(terminators, peek)
353
+ break
354
+ end
355
+ str << peek # not a terminator
356
+ grab
357
+ end
358
+
359
+ if peek == "]" # skip right bracket
360
+ grab
361
+ end
362
+ add str
363
+ str
364
+ rescue => err
365
+ ::STDERR.puts "ERR = #{err}\n#{err.backtrace}"
366
+ end
367
+
368
+ def escaped
369
+ grab # Eat the backslash
370
+ ch = grab # Take next char
371
+ ch
372
+ end
373
+
374
+ def collect!(sym, terminators, bracketed=nil)
375
+ return collect_bracketed(sym, terminators) if bracketed
376
+ str = Null.dup # next is not " ","*","["
377
+ grab # ZZZ
378
+ loop do
379
+ case
380
+ when peek.nil?
381
+ return str
382
+ when peek == Escape
383
+ str << escaped
384
+ next
385
+ when terminate?(terminators, peek)
386
+ break
387
+ else
388
+ str << peek # not a terminator
389
+ end
390
+ grab
391
+ end
392
+ ungrab
393
+ add str
394
+ str
395
+ rescue => err
396
+ ::STDERR.puts "ERR = #{err}\n#{err.backtrace}"
397
+ end
398
+
399
+ def varsub(name)
400
+ live = Livetext.new
401
+ value = live.vars[name]
402
+ result = value || "[#{name} is undefined]"
403
+ result
404
+ end
405
+
406
+ def embedded?
407
+ ! (['"', "'", " ", nil].include? prev)
408
+ end
409
+
410
+ # private
411
+
412
+ def eval_bits(sym, val)
413
+ # api.tty "eb: #{[sym, val].inspect}"
414
+ val = Livetext.interpolate(val)
415
+ @out << embed(sym, val)
416
+ end
417
+
418
+ def eval_func(val, gen)
419
+ param = nil
420
+ arg = gen.peek rescue :bogus
421
+ unless arg == :bogus
422
+ if [:colon, :brackets].include? arg[0]
423
+ arg = gen.next # for real
424
+ param = arg[1]
425
+ # FIXME - unsure - interpolate again??
426
+ # param = Livetext.interpolate(param)
427
+ end
428
+ end
429
+ str = funcall(val, param)
430
+ @out << str
431
+ end
432
+
433
+ def eval_var(val)
434
+ @out << varsub(val)
435
+ end
436
+
437
+ def eval_str(val)
438
+ @out << val unless val == "\n" # BUG
439
+ end
440
+
441
+ end
@@ -0,0 +1,158 @@
1
+
2
+ # Class Livetext reopened (top level).
3
+
4
+ class Livetext
5
+
6
+ include Helpers
7
+
8
+ class Variables
9
+ def initialize(hash = {})
10
+ @vars = {}
11
+ hash.each_pair {|k, v| @vars[k.to_sym] = v }
12
+ end
13
+
14
+ def [](var)
15
+ @vars[var.to_sym]
16
+ end
17
+
18
+ def []=(var, value)
19
+ @vars[var.to_sym] = value
20
+ end
21
+ end
22
+
23
+ Vars = Variables.new
24
+
25
+ TTY = ::File.open("/dev/tty", "w")
26
+
27
+ attr_reader :main, :sources
28
+ attr_accessor :nopass, :nopara
29
+ attr_accessor :body, :indentation
30
+
31
+ class << self
32
+ attr_accessor :output # bad solution?
33
+ end
34
+
35
+ def vars
36
+ @_vars
37
+ end
38
+
39
+ def self.interpolate(str)
40
+ parse = Livetext::LineParser.new(str)
41
+ parse.var_func_parse
42
+ end
43
+
44
+ def self.customize(mix: [], call: [], vars: {})
45
+ obj = self.new
46
+ mix = Array(mix)
47
+ call = Array(call)
48
+ mix.each {|lib| obj.mixin(lib) }
49
+ call.each {|cmd| obj.main.send(cmd[1..-1]) } # ignores leading dot, no param
50
+ vars.each_pair {|var, val| obj.setvar(var, val.to_s) }
51
+ obj
52
+ end
53
+
54
+ def peek_nextline
55
+ @main.peek_nextline # delegate
56
+ end
57
+
58
+ def nextline
59
+ @main.nextline # delegate
60
+ end
61
+
62
+ def sources
63
+ @main.sources # delegate
64
+ end
65
+
66
+ def save_location
67
+ @save_location # delegate
68
+ end
69
+
70
+ def save_location=(where)
71
+ @save_location = where # delegate
72
+ end
73
+
74
+ def dump(file = nil) # not a dot command!
75
+ file ||= ::STDOUT
76
+ file.puts @body
77
+ rescue => err
78
+ TTY.puts "#dump had an error: #{err.inspect}"
79
+ end
80
+
81
+ def graceful_error(err)
82
+ dump
83
+ raise err
84
+ end
85
+
86
+ def customize(mix: [], call: [], vars: {})
87
+ mix = Array(mix)
88
+ call = Array(call)
89
+ mix.each {|lib| mixin(lib) }
90
+ call.each {|cmd| @main.send(cmd[1..-1]) } # ignores leading dot, no param
91
+ vars.each_pair {|var, val| @api.setvar(var, val.to_s) }
92
+ self
93
+ end
94
+
95
+ def initialize(output = ::STDOUT)
96
+ @source = nil
97
+ @_mixins = []
98
+ @_imports = []
99
+ @_outdir = "."
100
+ @no_puts = output.nil?
101
+ @body = ""
102
+ @main = Processor.new(self, output)
103
+ @indentation = [0]
104
+ @_vars = Livetext::Vars
105
+ @api = UserAPI.new(self)
106
+ initial_vars
107
+ end
108
+
109
+ def api
110
+ @api
111
+ end
112
+
113
+ def initial_vars
114
+ # Other predefined variables (see also setfile)
115
+ @api.setvar(:User, `whoami`.chomp)
116
+ @api.setvar(:Version, Livetext::VERSION)
117
+ end
118
+
119
+ def transform(text)
120
+ setfile!("(string)")
121
+ enum = text.each_line
122
+ front = text.match(/.*?\n/).to_a.first.chomp rescue ""
123
+ @main.source(enum, "STDIN: '#{front}...'", 0)
124
+ loop do
125
+ line = @main.nextline
126
+ break if line.nil?
127
+ process_line(line)
128
+ end
129
+ result = @body
130
+ # @body = ""
131
+ result
132
+ end
133
+
134
+ # EXPERIMENTAL and incomplete
135
+ def xform(*args, file: nil, text: nil, vars: {})
136
+ case
137
+ when file && text.nil?
138
+ xform_file(file)
139
+ when file.nil? && text
140
+ transform(text)
141
+ when file.nil? && text.nil?
142
+ raise "Must specify file or text"
143
+ when file && text
144
+ raise "Cannot specify file and text"
145
+ end
146
+ self.process_file(file)
147
+ self.body
148
+ end
149
+
150
+ def xform_file(file, vars: nil)
151
+ Livetext::Vars.replace(vars) unless vars.nil?
152
+ @_vars.replace(vars) unless vars.nil?
153
+ self.process_file(file)
154
+ self.body
155
+ end
156
+
157
+ end
158
+
@@ -57,7 +57,9 @@ class Processor
57
57
  end
58
58
 
59
59
  def disallowed?(name)
60
- Disallowed.include?(name.to_sym)
60
+ flag = Disallowed.include?(name.to_sym)
61
+ # api.tty "disa name = #{name.inspect} flag = #{flag}"
62
+ flag
61
63
  end
62
64
 
63
65
  def source(enum, file, line)
@@ -9,6 +9,11 @@ class Livetext
9
9
  module ParsingConstants
10
10
  end
11
11
 
12
+ class LineParser < StringParser
13
+ module FunCall
14
+ end
15
+ end
16
+
12
17
  class FormatLine < StringParser
13
18
  module FunCall
14
19
  end
@@ -25,13 +25,13 @@ module Livetext::Standard
25
25
 
26
26
  attr_reader :data
27
27
 
28
- def data=(val) # FIXME this is weird, let's remove it soonish
29
- api.data = val.chomp
30
- # @args = val.split rescue []
28
+ def data=(val) # FIXME this is weird, let's remove it soonish and why are there two???
29
+ # api.tty ">>>> in #{__FILE__}: api id = #{api.object_id}"
30
+ val = val.chomp
31
+ api.data = val
32
+ api.args = format(val).split rescue []
31
33
  @mixins = []
32
34
  @imports = []
33
- ###
34
- # api.data = val
35
35
  end
36
36
 
37
37
  # dumb name - bold, italic, teletype, striketrough
@@ -143,10 +143,14 @@ module Livetext::Standard
143
143
  end
144
144
 
145
145
  def dot_def(args = nil, body = nil)
146
+ # api.tty "in #{__FILE__}: api id = #{api.inspect}"
146
147
  name = api.args[0]
147
- str = "def #{name}\n"
148
+ # api.tty :dd1
149
+ # api.tty name.inspect
148
150
  check_disallowed(name)
151
+ # api.tty :dd2
149
152
  # Difficult to avoid eval here
153
+ str = "def #{name}\n"
150
154
  str << api.body(true).join("\n")
151
155
  str << "\nend\n"
152
156
  eval str
@@ -172,7 +176,7 @@ module Livetext::Standard
172
176
  else
173
177
  lines = api.body
174
178
  end
175
- pairs = Livetext::ParseGeneral.parse_vars(prefix, lines)
179
+ pairs = Livetext::ParseGeneral.parse_vars(lines, prefix: nil)
176
180
  set_variables(pairs)
177
181
  api.optional_blank_line
178
182
  end
@@ -187,7 +191,7 @@ module Livetext::Standard
187
191
  else
188
192
  lines = api.body
189
193
  end
190
- pairs = Livetext::ParseGeneral.parse_vars(prefix, lines)
194
+ pairs = Livetext::ParseGeneral.parse_vars(lines, prefix: nil)
191
195
  set_variables(pairs)
192
196
  api.optional_blank_line
193
197
  end