rivescript 0.1.1
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 +7 -0
- data/Changes.md +13 -0
- data/LICENSE +22 -0
- data/README.md +131 -0
- data/bin/riveshell +81 -0
- data/eg/brain/admin.rive +15 -0
- data/eg/brain/begin.rive +190 -0
- data/eg/brain/clients.rive +72 -0
- data/eg/brain/eliza.rive +301 -0
- data/eg/brain/myself.rive +61 -0
- data/eg/brain/rpg.rive +294 -0
- data/lib/rivescript/brain.rb +986 -0
- data/lib/rivescript/inheritance.rb +105 -0
- data/lib/rivescript/lang/ruby.rb +72 -0
- data/lib/rivescript/parser.rb +581 -0
- data/lib/rivescript/sessions.rb +205 -0
- data/lib/rivescript/sorting.rb +158 -0
- data/lib/rivescript/utils.rb +116 -0
- data/lib/rivescript/version.rb +9 -0
- data/lib/rivescript.rb +460 -0
- metadata +69 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
# RiveScript Ruby port, https://jvmlab.org/, MIT License
|
|
2
|
+
|
|
3
|
+
require_relative "utils"
|
|
4
|
+
|
|
5
|
+
class RiveScript
|
|
6
|
+
# The version of the RiveScript language we support.
|
|
7
|
+
RS_VERSION = "2.0"
|
|
8
|
+
|
|
9
|
+
# Parser for RiveScript syntax.
|
|
10
|
+
class Parser
|
|
11
|
+
CONCAT_MODES = {
|
|
12
|
+
"none" => "",
|
|
13
|
+
"newline" => "\n",
|
|
14
|
+
"space" => " "
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(master)
|
|
18
|
+
@master = master
|
|
19
|
+
@strict = master._strict
|
|
20
|
+
@utf8 = master._utf8
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Proxy functions
|
|
24
|
+
def say(message)
|
|
25
|
+
@master.say(message)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def warn(message, filename = nil, lineno = nil)
|
|
29
|
+
@master.warn(message, filename, lineno)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Read and parse a RiveScript document.
|
|
33
|
+
def parse(filename, code, on_error = nil)
|
|
34
|
+
on_error ||= lambda { |err, fname, lineno| warn(err, fname, lineno) }
|
|
35
|
+
|
|
36
|
+
ast = {
|
|
37
|
+
"begin" => {
|
|
38
|
+
"global" => {},
|
|
39
|
+
"var" => {},
|
|
40
|
+
"sub" => {},
|
|
41
|
+
"person" => {},
|
|
42
|
+
"array" => {}
|
|
43
|
+
},
|
|
44
|
+
"topics" => {},
|
|
45
|
+
"objects" => []
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
topic = "random"
|
|
49
|
+
comment = false
|
|
50
|
+
inobj = false
|
|
51
|
+
obj_name = ""
|
|
52
|
+
obj_lang = ""
|
|
53
|
+
obj_buf = []
|
|
54
|
+
cur_trig = nil
|
|
55
|
+
is_that = nil
|
|
56
|
+
|
|
57
|
+
local_options = {
|
|
58
|
+
"concat" => @master._concat.nil? ? "none" : @master._concat
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
lines = code.split("\n")
|
|
62
|
+
lines.each_with_index do |raw_line, lp|
|
|
63
|
+
line = Utils.strip(raw_line)
|
|
64
|
+
lineno = lp + 1
|
|
65
|
+
|
|
66
|
+
next if line.empty?
|
|
67
|
+
|
|
68
|
+
if inobj
|
|
69
|
+
if line.include?("< object") || line.include?("<object")
|
|
70
|
+
if !obj_name.empty?
|
|
71
|
+
ast["objects"] << {
|
|
72
|
+
"name" => obj_name,
|
|
73
|
+
"language" => obj_lang,
|
|
74
|
+
"code" => obj_buf
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
obj_name = ""
|
|
78
|
+
obj_lang = ""
|
|
79
|
+
obj_buf = []
|
|
80
|
+
inobj = false
|
|
81
|
+
else
|
|
82
|
+
obj_buf << line
|
|
83
|
+
end
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if line.start_with?("//")
|
|
88
|
+
next
|
|
89
|
+
elsif line.start_with?("#")
|
|
90
|
+
warn("Using the # symbol for comments is deprecated", filename, lineno)
|
|
91
|
+
next
|
|
92
|
+
elsif line.start_with?("/*")
|
|
93
|
+
if line.include?("*/")
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
comment = true
|
|
98
|
+
next
|
|
99
|
+
elsif line.include?("*/")
|
|
100
|
+
comment = false
|
|
101
|
+
next
|
|
102
|
+
end
|
|
103
|
+
next if comment
|
|
104
|
+
|
|
105
|
+
if line.length < 2
|
|
106
|
+
warn("Weird single-character line '#{line}' found (in topic #{topic})", filename, lineno)
|
|
107
|
+
next
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
cmd = line[0]
|
|
111
|
+
line = Utils.strip(line[1..])
|
|
112
|
+
|
|
113
|
+
if line.include?(" //")
|
|
114
|
+
line = Utils.strip(line.split(" //", 2)[0])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if cmd == "?"
|
|
118
|
+
variants = [
|
|
119
|
+
line,
|
|
120
|
+
"[*]#{line}[*]",
|
|
121
|
+
"*#{line}*",
|
|
122
|
+
"[*]#{line}*",
|
|
123
|
+
"*#{line}[*]",
|
|
124
|
+
"#{line}*",
|
|
125
|
+
"*#{line}"
|
|
126
|
+
]
|
|
127
|
+
cmd = "+"
|
|
128
|
+
line = "(#{variants.join('|')})"
|
|
129
|
+
say("Rewrote ?Keyword as +Trigger: #{line}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if @master._forceCase == true && cmd == "+"
|
|
133
|
+
line = line.downcase
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
syntax_error = check_syntax(cmd, line)
|
|
137
|
+
unless syntax_error.empty?
|
|
138
|
+
if @strict
|
|
139
|
+
on_error.call("Syntax error: #{syntax_error} at #{filename} line #{lineno} near #{cmd} #{line}", filename, lineno)
|
|
140
|
+
else
|
|
141
|
+
warn("Syntax error: #{syntax_error} at #{filename} line #{lineno} near #{cmd} #{line} (in topic #{topic})", filename, lineno)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
is_that = nil if cmd == "+"
|
|
146
|
+
|
|
147
|
+
say("Cmd: #{cmd}; line: #{line}")
|
|
148
|
+
|
|
149
|
+
((lp + 1)...lines.length).each do |li|
|
|
150
|
+
lookahead = Utils.strip(lines[li])
|
|
151
|
+
next if lookahead.length < 2
|
|
152
|
+
|
|
153
|
+
look_cmd = lookahead[0]
|
|
154
|
+
lookahead = Utils.strip(lookahead[1..])
|
|
155
|
+
|
|
156
|
+
break unless ["%", "^"].include?(look_cmd)
|
|
157
|
+
break if lookahead.empty?
|
|
158
|
+
|
|
159
|
+
say("\tLookahead #{li}: #{look_cmd} #{lookahead}")
|
|
160
|
+
|
|
161
|
+
if cmd == "+"
|
|
162
|
+
if look_cmd == "%"
|
|
163
|
+
is_that = lookahead
|
|
164
|
+
break
|
|
165
|
+
else
|
|
166
|
+
is_that = nil
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if cmd == "!"
|
|
171
|
+
if look_cmd == "^"
|
|
172
|
+
line += "<crlf>#{lookahead}"
|
|
173
|
+
end
|
|
174
|
+
next
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if cmd != "^" && look_cmd != "%"
|
|
178
|
+
if look_cmd == "^"
|
|
179
|
+
if CONCAT_MODES.key?(local_options["concat"])
|
|
180
|
+
line += CONCAT_MODES[local_options["concat"]] + lookahead
|
|
181
|
+
else
|
|
182
|
+
line += lookahead
|
|
183
|
+
end
|
|
184
|
+
else
|
|
185
|
+
break
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
type = ""
|
|
191
|
+
name = ""
|
|
192
|
+
|
|
193
|
+
case cmd
|
|
194
|
+
when "!"
|
|
195
|
+
halves = line.split("=", 2)
|
|
196
|
+
left = Utils.strip(halves[0]).split(" ")
|
|
197
|
+
value = ""
|
|
198
|
+
name = ""
|
|
199
|
+
type = ""
|
|
200
|
+
value = Utils.strip(halves[1]) if halves.length == 2
|
|
201
|
+
|
|
202
|
+
if left.length >= 1
|
|
203
|
+
type = Utils.strip(left[0])
|
|
204
|
+
if left.length >= 2
|
|
205
|
+
left.shift
|
|
206
|
+
name = Utils.strip(left.join(" "))
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
value = value.gsub("<crlf>", "") unless type == "array"
|
|
211
|
+
|
|
212
|
+
if type == "version"
|
|
213
|
+
if value.to_f > RS_VERSION.to_f
|
|
214
|
+
on_error.call("Unsupported RiveScript version. We only support #{RS_VERSION} at #{filename} line #{lineno}", filename, lineno)
|
|
215
|
+
return ast
|
|
216
|
+
end
|
|
217
|
+
next
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
if name.empty?
|
|
221
|
+
warn("Undefined variable name", filename, lineno)
|
|
222
|
+
next
|
|
223
|
+
end
|
|
224
|
+
if value.empty?
|
|
225
|
+
warn("Undefined variable value", filename, lineno)
|
|
226
|
+
next
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
case type
|
|
230
|
+
when "local"
|
|
231
|
+
say("\tSet local parser option #{name} = #{value}")
|
|
232
|
+
local_options[name] = value
|
|
233
|
+
when "global"
|
|
234
|
+
say("\tSet global #{name} = #{value}")
|
|
235
|
+
ast["begin"]["global"][name] = value
|
|
236
|
+
when "var"
|
|
237
|
+
say("\tSet bot variable #{name} = #{value}")
|
|
238
|
+
ast["begin"]["var"][name] = value
|
|
239
|
+
when "array"
|
|
240
|
+
if value == "<undef>"
|
|
241
|
+
ast["begin"]["array"][name] = "<undef>"
|
|
242
|
+
next
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
parts = value.split("<crlf>")
|
|
246
|
+
fields = []
|
|
247
|
+
parts.each do |val|
|
|
248
|
+
if val.include?("|")
|
|
249
|
+
fields.concat(val.split("|"))
|
|
250
|
+
else
|
|
251
|
+
fields.concat(val.split(" "))
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
fields.map! { |field| field.gsub(/\\s/i, " ") }
|
|
256
|
+
fields.reject!(&:empty?)
|
|
257
|
+
|
|
258
|
+
say("\tSet array #{name} = #{fields.inspect}")
|
|
259
|
+
ast["begin"]["array"][name] = fields
|
|
260
|
+
when "sub"
|
|
261
|
+
say("\tSet substitution #{name} = #{value}")
|
|
262
|
+
ast["begin"]["sub"][name] = value
|
|
263
|
+
when "person"
|
|
264
|
+
say("\tSet person substitution #{name} = #{value}")
|
|
265
|
+
ast["begin"]["person"][name] = value
|
|
266
|
+
else
|
|
267
|
+
warn("Unknown definition type #{type}", filename, lineno)
|
|
268
|
+
end
|
|
269
|
+
when ">"
|
|
270
|
+
temp = Utils.strip(line).split(" ")
|
|
271
|
+
type = temp.shift
|
|
272
|
+
name = ""
|
|
273
|
+
fields = []
|
|
274
|
+
name = temp.shift if temp.length > 0
|
|
275
|
+
fields = temp if temp.length > 0
|
|
276
|
+
|
|
277
|
+
case type
|
|
278
|
+
when "begin", "topic"
|
|
279
|
+
if type == "begin"
|
|
280
|
+
say("Found the BEGIN block.")
|
|
281
|
+
type = "topic"
|
|
282
|
+
name = "__begin__"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
name = name.downcase if @master._forceCase == true
|
|
286
|
+
|
|
287
|
+
say("Set topic to #{name}")
|
|
288
|
+
cur_trig = nil
|
|
289
|
+
topic = name
|
|
290
|
+
|
|
291
|
+
init_topic(ast["topics"], topic)
|
|
292
|
+
|
|
293
|
+
mode = ""
|
|
294
|
+
if fields.length >= 2
|
|
295
|
+
fields.each do |field|
|
|
296
|
+
if ["includes", "inherits"].include?(field)
|
|
297
|
+
mode = field
|
|
298
|
+
elsif !mode.empty?
|
|
299
|
+
ast["topics"][topic][mode][field] = 1
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
when "object"
|
|
304
|
+
lang = ""
|
|
305
|
+
lang = fields[0].downcase if fields.length > 0
|
|
306
|
+
|
|
307
|
+
if lang.empty?
|
|
308
|
+
warn("Trying to parse unknown programming language", filename, lineno)
|
|
309
|
+
lang = "ruby"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
obj_name = name
|
|
313
|
+
obj_lang = lang
|
|
314
|
+
obj_buf = []
|
|
315
|
+
inobj = true
|
|
316
|
+
else
|
|
317
|
+
warn("Unknown label type #{type}", filename, lineno)
|
|
318
|
+
end
|
|
319
|
+
when "<"
|
|
320
|
+
type = line
|
|
321
|
+
if ["begin", "topic"].include?(type)
|
|
322
|
+
say("\tEnd the topic label.")
|
|
323
|
+
topic = "random"
|
|
324
|
+
elsif type == "object"
|
|
325
|
+
say("\tEnd the object label.")
|
|
326
|
+
inobj = false
|
|
327
|
+
end
|
|
328
|
+
when "+"
|
|
329
|
+
say("\tTrigger pattern: #{line}")
|
|
330
|
+
|
|
331
|
+
init_topic(ast["topics"], topic)
|
|
332
|
+
cur_trig = {
|
|
333
|
+
"trigger" => line,
|
|
334
|
+
"reply" => [],
|
|
335
|
+
"condition" => [],
|
|
336
|
+
"redirect" => nil,
|
|
337
|
+
"previous" => is_that
|
|
338
|
+
}
|
|
339
|
+
ast["topics"][topic]["triggers"] << cur_trig
|
|
340
|
+
when "-"
|
|
341
|
+
if cur_trig.nil?
|
|
342
|
+
warn("Response found before trigger", filename, lineno)
|
|
343
|
+
next
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
if !cur_trig["redirect"].nil?
|
|
347
|
+
warn("You can't mix @Redirects with -Replies", filename, lineno)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
say("\tResponse: #{line}")
|
|
351
|
+
cur_trig["reply"] << line
|
|
352
|
+
when "*"
|
|
353
|
+
if cur_trig.nil?
|
|
354
|
+
warn("Condition found before trigger", filename, lineno)
|
|
355
|
+
next
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
if !cur_trig["redirect"].nil?
|
|
359
|
+
warn("You can't mix @Redirects with *Conditions", filename, lineno)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
say("\tCondition: #{line}")
|
|
363
|
+
cur_trig["condition"] << line
|
|
364
|
+
when "%", "^"
|
|
365
|
+
next
|
|
366
|
+
when "@"
|
|
367
|
+
if cur_trig["reply"].length > 0 || cur_trig["condition"].length > 0
|
|
368
|
+
warn("You can't mix @Redirects with -Replies or *Conditions", filename, lineno)
|
|
369
|
+
end
|
|
370
|
+
say("\tRedirect response to: #{line}")
|
|
371
|
+
cur_trig["redirect"] = Utils.strip(line)
|
|
372
|
+
else
|
|
373
|
+
warn("Unknown command '#{cmd}' (in topic #{topic})", filename, lineno)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
ast
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Translate deparsed data into the source code of a RiveScript document.
|
|
381
|
+
def stringify(deparsed = nil)
|
|
382
|
+
deparsed = @master.deparse if deparsed.nil?
|
|
383
|
+
|
|
384
|
+
write_triggers = lambda do |triggers, indent|
|
|
385
|
+
id = indent ? "\t" : ""
|
|
386
|
+
output = []
|
|
387
|
+
triggers.each do |t|
|
|
388
|
+
output << "#{id}+ #{t['trigger']}"
|
|
389
|
+
output << "#{id}% #{t['previous']}" if t["previous"]
|
|
390
|
+
t["condition"]&.each do |c|
|
|
391
|
+
output << "#{id}* #{c.gsub("\n", "\\n")}"
|
|
392
|
+
end
|
|
393
|
+
output << "#{id}@ #{t['redirect']}" if t["redirect"]
|
|
394
|
+
t["reply"]&.each do |r|
|
|
395
|
+
output << "#{id}- #{r.gsub("\n", "\\n")}" if r
|
|
396
|
+
end
|
|
397
|
+
output << ""
|
|
398
|
+
end
|
|
399
|
+
output
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
source = ["! version = 2.0", "! local concat = none", ""]
|
|
403
|
+
ref = ["global", "var", "sub", "person", "array"]
|
|
404
|
+
|
|
405
|
+
ref.each do |begin_type|
|
|
406
|
+
next if deparsed["begin"][begin_type].nil? || deparsed["begin"][begin_type].empty?
|
|
407
|
+
|
|
408
|
+
deparsed["begin"][begin_type].each do |key, value|
|
|
409
|
+
if begin_type != "array"
|
|
410
|
+
source << "! #{begin_type} #{key} = #{value}"
|
|
411
|
+
else
|
|
412
|
+
pipes = " "
|
|
413
|
+
value.each do |test|
|
|
414
|
+
if test.match?(/\s+/)
|
|
415
|
+
pipes = "|"
|
|
416
|
+
break
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
source << "! #{begin_type} #{key} = #{value.join(pipes)}"
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
source << ""
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
if deparsed["objects"]
|
|
426
|
+
deparsed["objects"].each do |lang, lang_objects|
|
|
427
|
+
next unless lang_objects && lang_objects["_objects"]
|
|
428
|
+
|
|
429
|
+
sources = lang_objects["_sources"] || {}
|
|
430
|
+
lang_objects["_objects"].each do |func, code|
|
|
431
|
+
source << "> object #{func} #{lang}"
|
|
432
|
+
if sources[func]
|
|
433
|
+
source << sources[func].to_s.split("\n").map { |ln| "\t#{ln}" }.join("\n")
|
|
434
|
+
elsif code.is_a?(String)
|
|
435
|
+
body = code.to_s.match(/function[^{]+\{\n*([\s\S]*)\};?\s*$/m)
|
|
436
|
+
source << body[1].strip.split("\n").map { |ln| "\t#{ln}" }.join("\n") if body
|
|
437
|
+
elsif code.respond_to?(:source)
|
|
438
|
+
# no-op for procs without source
|
|
439
|
+
end
|
|
440
|
+
source << "< object\n"
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
if deparsed["begin"]["triggers"] && deparsed["begin"]["triggers"].length > 0
|
|
446
|
+
source << "> begin\n"
|
|
447
|
+
source.concat(write_triggers.call(deparsed["begin"]["triggers"], "indent"))
|
|
448
|
+
source << "< begin\n"
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
topics = deparsed["topics"].keys.sort
|
|
452
|
+
topics.unshift("random")
|
|
453
|
+
done_random = false
|
|
454
|
+
|
|
455
|
+
topics.each do |topic_name|
|
|
456
|
+
next unless deparsed["topics"].key?(topic_name)
|
|
457
|
+
next if topic_name == "random" && done_random
|
|
458
|
+
|
|
459
|
+
done_random = true if topic_name == "random"
|
|
460
|
+
|
|
461
|
+
tagged = false
|
|
462
|
+
tagline = []
|
|
463
|
+
if topic_name != "random" ||
|
|
464
|
+
(!(deparsed["inherits"][topic_name] || {}).empty? || !(deparsed["includes"][topic_name] || {}).empty?)
|
|
465
|
+
tagged = true if topic_name != "random"
|
|
466
|
+
|
|
467
|
+
inherits = (deparsed["inherits"][topic_name] || {}).keys
|
|
468
|
+
includes = (deparsed["includes"][topic_name] || {}).keys
|
|
469
|
+
|
|
470
|
+
if includes.length > 0
|
|
471
|
+
tagline.concat(["includes"] + includes)
|
|
472
|
+
tagged = true
|
|
473
|
+
end
|
|
474
|
+
if inherits.length > 0
|
|
475
|
+
tagline.concat(["inherits"] + inherits)
|
|
476
|
+
tagged = true
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
if tagged
|
|
481
|
+
source << ("> topic #{topic_name} " + tagline.join(" ")).strip + "\n"
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
source.concat(write_triggers.call(deparsed["topics"][topic_name], tagged))
|
|
485
|
+
|
|
486
|
+
source << "< topic\n" if tagged
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
source.join("\n")
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Check the syntax of a RiveScript command.
|
|
493
|
+
def check_syntax(cmd, line)
|
|
494
|
+
case cmd
|
|
495
|
+
when "!"
|
|
496
|
+
unless line.match?(/\A.+(?:\s+.+|)\s*=\s*.+?\z/)
|
|
497
|
+
return "Invalid format for !Definition line: must be '! type name = value' OR '! type = value'"
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
if line.match?(/^array/)
|
|
501
|
+
if line.match?(/=\s?\||\|\s?$/)
|
|
502
|
+
return "Piped arrays can't begin or end with a |"
|
|
503
|
+
elsif line.match?(/\|\|/)
|
|
504
|
+
return "Piped arrays can't include blank entries"
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
when ">"
|
|
508
|
+
parts = line.split(/\s+/)
|
|
509
|
+
if parts[0] == "begin" && parts.length > 1
|
|
510
|
+
return "The 'begin' label takes no additional arguments"
|
|
511
|
+
elsif parts[0] == "topic"
|
|
512
|
+
if !@master._forceCase && line.match?(/[^a-z0-9_\-\s]/)
|
|
513
|
+
return "Topics should be lowercased and contain only letters and numbers"
|
|
514
|
+
elsif line.match?(/[^A-Za-z0-9_\-\s]/)
|
|
515
|
+
return "Topics should contain only letters and numbers in forceCase mode"
|
|
516
|
+
end
|
|
517
|
+
elsif parts[0] == "object"
|
|
518
|
+
if line.match?(/[^A-Za-z0-9_\-\s]/)
|
|
519
|
+
return "Objects can only contain numbers and letters"
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
when "+", "%", "@"
|
|
523
|
+
parens = 0
|
|
524
|
+
square = 0
|
|
525
|
+
curly = 0
|
|
526
|
+
angle = 0
|
|
527
|
+
|
|
528
|
+
if @utf8
|
|
529
|
+
if line.match?(/[A-Z\\.]/)
|
|
530
|
+
return "Triggers can't contain uppercase letters, backslashes or dots in UTF-8 mode"
|
|
531
|
+
end
|
|
532
|
+
elsif line.match?(/[^a-z0-9(|)\[\]*_#@{}<>=\/\s]/)
|
|
533
|
+
return "Triggers may only contain lowercase letters, numbers, and these symbols: ( | ) [ ] * _ # { } < > = /"
|
|
534
|
+
elsif line.match?(/\(\||\|\)/)
|
|
535
|
+
return "Piped alternations can't begin or end with a |"
|
|
536
|
+
elsif line.match?(/\([^\)].+\|\|.+\)/)
|
|
537
|
+
return "Piped alternations can't include blank entries"
|
|
538
|
+
elsif line.match?(/\[\||\|\]/)
|
|
539
|
+
return "Piped optionals can't begin or end with a |"
|
|
540
|
+
elsif line.match?(/\[[^\]].+\|\|.+\]/)
|
|
541
|
+
return "Piped optionals can't include blank entries"
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
line.each_char do |char|
|
|
545
|
+
case char
|
|
546
|
+
when "(" then parens += 1
|
|
547
|
+
when ")" then parens -= 1
|
|
548
|
+
when "[" then square += 1
|
|
549
|
+
when "]" then square -= 1
|
|
550
|
+
when "{" then curly += 1
|
|
551
|
+
when "}" then curly -= 1
|
|
552
|
+
when "<" then angle += 1
|
|
553
|
+
when ">" then angle -= 1
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
return "Unmatched parenthesis brackets" if parens != 0
|
|
558
|
+
return "Unmatched square brackets" if square != 0
|
|
559
|
+
return "Unmatched curly brackets" if curly != 0
|
|
560
|
+
return "Unmatched angle brackets" if angle != 0
|
|
561
|
+
when "*"
|
|
562
|
+
unless line.match?(/\A.+?\s*(?:==|eq|!=|ne|<>|<|<=|>|>=)\s*.+?=>.+?\z/)
|
|
563
|
+
return "Invalid format for !Condition: should be like '* value symbol value => response'"
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
""
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Initialize the topic tree for the parsing phase.
|
|
571
|
+
def init_topic(topics, name)
|
|
572
|
+
return unless topics[name].nil?
|
|
573
|
+
|
|
574
|
+
topics[name] = {
|
|
575
|
+
"includes" => {},
|
|
576
|
+
"inherits" => {},
|
|
577
|
+
"triggers" => []
|
|
578
|
+
}
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
end
|