citrus 2.4.1 → 2.5.0

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.
data/CHANGES CHANGED
@@ -1,3 +1,16 @@
1
+ = 2.5.0 / 2014-03-13
2
+
3
+ * Inputs may be generated from many different sources, including Pathname and
4
+ IO objects (thanks blambeau).
5
+
6
+ * Matches keep track of their offset in the original source (thanks
7
+ blambeau).
8
+
9
+ * Citrus.load no longer raises Citrus::LoadError for files that can't be found
10
+ or are not readable. Users must rescue Errno::ENOENT instead, for example.
11
+
12
+ * Removed a few ruby warnings (thanks tbuehlmann)
13
+
1
14
  = 2.4.1 / 2011-11-04
2
15
 
3
16
  * Fixed a bug that prevented rule names from starting with "super".
data/Rakefile CHANGED
@@ -15,7 +15,7 @@ desc "Generate API documentation"
15
15
  task :api => 'lib/citrus.rb' do |t|
16
16
  output_dir = ENV['OUTPUT_DIR'] || 'api'
17
17
  rm_rf output_dir
18
- sh((<<-SH).gsub(/[\s\n]+/, ' ').strip)
18
+ sh((<<-SH).gsub(/\s+/, ' ').strip)
19
19
  hanna
20
20
  --op #{output_dir}
21
21
  --promiscuous
@@ -73,9 +73,6 @@ module Citrus
73
73
  force = options.delete(:force)
74
74
 
75
75
  if force || !cache[file]
76
- raise LoadError, "Cannot find file #{file}" unless ::File.file?(file)
77
- raise LoadError, "Cannot read file #{file}" unless ::File.readable?(file)
78
-
79
76
  begin
80
77
  cache[file] = eval(::File.read(file), options)
81
78
  rescue SyntaxError => e
@@ -117,7 +114,10 @@ module Citrus
117
114
  end
118
115
 
119
116
  # A base class for all Citrus errors.
120
- class Error < RuntimeError; end
117
+ class Error < StandardError; end
118
+
119
+ # Raised when Citrus.require can't find the file to load.
120
+ class LoadError < Error; end
121
121
 
122
122
  # Raised when a parse fails.
123
123
  class ParseError < Error
@@ -156,9 +156,6 @@ module Citrus
156
156
  end
157
157
  end
158
158
 
159
- # Raised when Citrus.load fails to load a file.
160
- class LoadError < Error; end
161
-
162
159
  # Raised when Citrus::File.parse fails.
163
160
  class SyntaxError < Error
164
161
  # The +error+ given here is an instance of Citrus::ParseError.
@@ -173,14 +170,19 @@ module Citrus
173
170
  # An Input is a scanner that is responsible for executing rules at different
174
171
  # positions in the input string and persisting event streams.
175
172
  class Input < StringScanner
176
- def initialize(string)
177
- super(string)
173
+ def initialize(source)
174
+ super(source_text(source))
175
+ @source = source
178
176
  @max_offset = 0
179
177
  end
180
178
 
181
179
  # The maximum offset in the input that was successfully parsed.
182
180
  attr_reader :max_offset
183
181
 
182
+ # The initial source passed at construction. Typically a String
183
+ # or a Pathname.
184
+ attr_reader :source
185
+
184
186
  def reset # :nodoc:
185
187
  @max_offset = 0
186
188
  super
@@ -263,8 +265,24 @@ module Citrus
263
265
  events[-1]
264
266
  end
265
267
 
268
+ # Returns the scanned string.
269
+ alias_method :to_str, :string
270
+
266
271
  private
267
272
 
273
+ # Returns the text to parse from +source+.
274
+ def source_text(source)
275
+ if source.respond_to?(:to_path)
276
+ ::File.read(source.to_path)
277
+ elsif source.respond_to?(:read)
278
+ source.read
279
+ elsif source.respond_to?(:to_str)
280
+ source.to_str
281
+ else
282
+ raise ArgumentError, "Unable to parse from #{source}", caller
283
+ end
284
+ end
285
+
268
286
  # Appends all events for +rule+ at the given +position+ to +events+.
269
287
  def apply_rule(rule, position, events)
270
288
  rule.exec(self, events)
@@ -361,17 +379,24 @@ module Citrus
361
379
  super
362
380
  end
363
381
 
364
- # Parses the given +string+ using this grammar's root rule. Accepts the same
382
+ # Parses the given +source+ using this grammar's root rule. Accepts the same
365
383
  # +options+ as Rule#parse, plus the following:
366
384
  #
367
385
  # root:: The name of the root rule to start parsing at. Defaults to this
368
386
  # grammar's #root.
369
- def parse(string, options={})
387
+ def parse(source, options={})
370
388
  rule_name = options.delete(:root) || root
371
389
  raise Error, "No root rule specified" unless rule_name
372
390
  rule = rule(rule_name)
373
391
  raise Error, "No rule named \"#{rule_name}\"" unless rule
374
- rule.parse(string, options)
392
+ rule.parse(source, options)
393
+ end
394
+
395
+ # Parses the contents of the file at the given +path+ using this grammar's
396
+ # #root rule. Accepts the same +options+ as #parse.
397
+ def parse_file(path, options={})
398
+ path = Pathname.new(path.to_str) unless Pathname === path
399
+ parse(path, options)
375
400
  end
376
401
 
377
402
  # Returns the name of this grammar as a string.
@@ -455,9 +480,16 @@ module Citrus
455
480
  # Gets/sets the +name+ of the root rule of this grammar. If no root rule is
456
481
  # explicitly specified, the name of this grammar's first rule is returned.
457
482
  def root(name=nil)
458
- @root = name.to_sym if name
459
- # The first rule in a grammar is the default root.
460
- @root || rule_names.first
483
+ if name
484
+ @root = name.to_sym
485
+ else
486
+ # The first rule in a grammar is the default root.
487
+ if instance_variable_defined?(:@root)
488
+ @root
489
+ else
490
+ rule_names.first
491
+ end
492
+ end
461
493
  end
462
494
 
463
495
  # Creates a new rule that will match any single character. A block may be
@@ -623,10 +655,11 @@ module Citrus
623
655
  # +false+.
624
656
  # offset:: The offset in +string+ at which to start parsing. Defaults
625
657
  # to 0.
626
- def parse(string, options={})
658
+ def parse(source, options={})
627
659
  opts = default_options.merge(options)
628
660
 
629
- input = (opts[:memoize] ? MemoizedInput : Input).new(string)
661
+ input = (opts[:memoize] ? MemoizedInput : Input).new(source)
662
+ string = input.string
630
663
  input.pos = opts[:offset] if opts[:offset] > 0
631
664
 
632
665
  events = input.exec(self)
@@ -636,7 +669,7 @@ module Citrus
636
669
  raise ParseError, input
637
670
  end
638
671
 
639
- Match.new(string.slice(opts[:offset], length), events)
672
+ Match.new(input, events, opts[:offset])
640
673
  end
641
674
 
642
675
  # Tests whether or not this rule matches on the given +string+. Returns the
@@ -1239,14 +1272,13 @@ module Citrus
1239
1272
  # instantiated as needed. This class provides several convenient tree
1240
1273
  # traversal methods that help when examining and interpreting parse results.
1241
1274
  class Match
1242
- def initialize(string, events=[])
1243
- @string = string
1275
+ def initialize(input, events=[], offset=0)
1276
+ @input = input
1277
+ @offset = offset
1278
+ @captures = nil
1279
+ @matches = nil
1244
1280
 
1245
1281
  if events.length > 0
1246
- if events[-1] != string.length
1247
- raise ArgumentError, "Invalid events for length #{string.length}"
1248
- end
1249
-
1250
1282
  elisions = []
1251
1283
 
1252
1284
  while events[0].elide?
@@ -1261,18 +1293,35 @@ module Citrus
1261
1293
  end
1262
1294
  else
1263
1295
  # Create a default stream of events for the given string.
1296
+ string = input.to_str
1264
1297
  events = [Rule.for(string), CLOSE, string.length]
1265
1298
  end
1266
1299
 
1267
1300
  @events = events
1268
1301
  end
1269
1302
 
1303
+ # The original Input this Match was generated on.
1304
+ attr_reader :input
1305
+
1306
+ # The index of this match in the #input.
1307
+ attr_reader :offset
1308
+
1270
1309
  # The array of events for this match.
1271
1310
  attr_reader :events
1272
1311
 
1273
1312
  # Returns the length of this match.
1274
1313
  def length
1275
- @string.length
1314
+ events.last
1315
+ end
1316
+
1317
+ # Convenient shortcut for +input.source+
1318
+ def source
1319
+ (input.respond_to?(:source) && input.source) || input
1320
+ end
1321
+
1322
+ # Returns the slice of the source text that this match captures.
1323
+ def string
1324
+ @string ||= input.to_str[offset, length]
1276
1325
  end
1277
1326
 
1278
1327
  # Returns a hash of capture names to arrays of matches with that name,
@@ -1296,16 +1345,18 @@ module Citrus
1296
1345
  # Allows methods of this match's string to be called directly and provides
1297
1346
  # a convenient interface for retrieving the first match with a given name.
1298
1347
  def method_missing(sym, *args, &block)
1299
- if @string.respond_to?(sym)
1300
- @string.__send__(sym, *args, &block)
1348
+ unless defined?(Citrus::METHOD_MISSING_WARNED)
1349
+ warn("[`#{sym}`] Citrus::Match#method_missing is unsafe and will be removed in 3.0. Use captures.")
1350
+ Citrus.send(:const_set, :METHOD_MISSING_WARNED, true)
1351
+ end
1352
+ if string.respond_to?(sym)
1353
+ string.__send__(sym, *args, &block)
1301
1354
  else
1302
1355
  captures[sym].first
1303
1356
  end
1304
1357
  end
1305
1358
 
1306
- def to_s
1307
- @string
1308
- end
1359
+ alias_method :to_s, :string
1309
1360
 
1310
1361
  # This alias allows strings to be compared to the string value of Match
1311
1362
  # objects. It is most useful in assertions in unit tests, e.g.:
@@ -1339,9 +1390,9 @@ module Citrus
1339
1390
  def ==(other)
1340
1391
  case other
1341
1392
  when String
1342
- @string == other
1393
+ string == other
1343
1394
  when Match
1344
- @string == other.to_s
1395
+ string == other.to_s
1345
1396
  else
1346
1397
  super
1347
1398
  end
@@ -1350,7 +1401,7 @@ module Citrus
1350
1401
  alias_method :eql?, :==
1351
1402
 
1352
1403
  def inspect
1353
- @string.inspect
1404
+ string.inspect
1354
1405
  end
1355
1406
 
1356
1407
  # Prints the entire subtree of this match using the given +indent+ to
@@ -1372,7 +1423,7 @@ module Citrus
1372
1423
  rule = stack.pop
1373
1424
 
1374
1425
  space = indent * (stack.size / 3)
1375
- string = @string.slice(os, event)
1426
+ string = self.string.slice(os, event)
1376
1427
  lines[start] = "#{space}#{string.inspect} rule=#{rule}, offset=#{os}, length=#{event}"
1377
1428
 
1378
1429
  last_length = event unless last_length
@@ -1425,7 +1476,7 @@ module Citrus
1425
1476
  os = stack.pop
1426
1477
  start = stack.pop
1427
1478
 
1428
- match = Match.new(@string.slice(os, event), @events[start..index])
1479
+ match = Match.new(input, @events[start..index], @offset + os)
1429
1480
  capture!(rule, match)
1430
1481
 
1431
1482
  if stack.size == 1
@@ -1518,6 +1569,7 @@ class Object
1518
1569
  # end
1519
1570
  #
1520
1571
  def grammar(name, &block)
1572
+ warn("Object#grammar will no longer be available by default in citrus 3.0; You should require 'citrus/core_ext'.")
1521
1573
  namespace = respond_to?(:const_set) ? self : Object
1522
1574
  namespace.const_set(name, Citrus::Grammar.new(&block))
1523
1575
  rescue NameError
@@ -165,7 +165,7 @@ module Citrus
165
165
  end
166
166
 
167
167
  rule :super do
168
- all('super', andp(" "), :space) {
168
+ ext(:super_keyword) {
169
169
  Super.new
170
170
  }
171
171
  end
@@ -329,6 +329,7 @@ module Citrus
329
329
  rule :grammar_keyword, [ /\bgrammar\b/, :space ]
330
330
  rule :root_keyword, [ /\broot\b/, :space ]
331
331
  rule :rule_keyword, [ /\brule\b/, :space ]
332
+ rule :super_keyword, [ /\bsuper\b/, :space ]
332
333
  rule :end_keyword, [ /\bend\b/, :space ]
333
334
 
334
335
  rule :constant, /[A-Z][a-zA-Z0-9_]*/
@@ -1,6 +1,6 @@
1
1
  module Citrus
2
2
  # The current version of Citrus as [major, minor, patch].
3
- VERSION = [2, 4, 1]
3
+ VERSION = [2, 5, 0]
4
4
 
5
5
  # Returns the current version of Citrus as a string.
6
6
  def self.version
@@ -0,0 +1 @@
1
+ rule super end
@@ -0,0 +1,3 @@
1
+ rule abc
2
+ "abc" | super
3
+ end
@@ -12,7 +12,6 @@ class AliasTest < Test::Unit::TestCase
12
12
  rule :b, 'abc'
13
13
  }
14
14
  rule_a = grammar.rule(:a)
15
- rule_b = grammar.rule(:b)
16
15
  events = rule_a.exec(Input.new('abc'))
17
16
  assert_equal([rule_a, CLOSE, 3], events)
18
17
  end
@@ -36,7 +35,6 @@ class AliasTest < Test::Unit::TestCase
36
35
  rule :b, :a
37
36
  }
38
37
  rule_b2 = grammar2.rule(:b)
39
- rule_a1 = grammar1.rule(:a)
40
38
  events = rule_b2.exec(Input.new('abc'))
41
39
  assert_equal([rule_b2, CLOSE, 3], events)
42
40
  end
@@ -74,7 +74,7 @@ class GrammarTest < Test::Unit::TestCase
74
74
  rule(:num) { all(1, 2, 3) }
75
75
  }
76
76
  assert_raise ParseError do
77
- match = grammar.parse('12')
77
+ grammar.parse('12')
78
78
  end
79
79
  end
80
80
 
@@ -99,7 +99,7 @@ class GrammarTest < Test::Unit::TestCase
99
99
  rule(:alphanum) { any(/[a-z]/, 0..9) }
100
100
  }
101
101
  assert_raise ParseError do
102
- match = grammar.parse('A')
102
+ grammar.parse('A')
103
103
  end
104
104
  end
105
105
 
@@ -126,6 +126,29 @@ class GrammarTest < Test::Unit::TestCase
126
126
  assert_equal(str.length, match.length)
127
127
  end
128
128
 
129
+ def test_parse_file
130
+ grammar = Grammar.new {
131
+ rule("words"){ rep(any(" ", /[a-z]+/)) }
132
+ }
133
+
134
+ require 'tempfile'
135
+ Tempfile.open('citrus') do |tmp|
136
+ tmp << "abd def"
137
+ tmp.close
138
+
139
+ match = grammar.parse_file(tmp.path)
140
+
141
+ assert(match)
142
+ assert_instance_of(Input, match.input)
143
+ assert_instance_of(Pathname, match.source)
144
+
145
+ match.matches.each do |m|
146
+ assert_instance_of(Input, m.input)
147
+ assert_instance_of(Pathname, m.source)
148
+ end
149
+ end
150
+ end
151
+
129
152
  def test_global_grammar
130
153
  assert_raise ArgumentError do
131
154
  grammar(:abc)
@@ -1,6 +1,21 @@
1
1
  require File.expand_path('../helper', __FILE__)
2
2
 
3
3
  class InputTest < Test::Unit::TestCase
4
+ def test_new
5
+ # to_str
6
+ assert_equal('abc', Input.new('abc').string)
7
+
8
+ # read
9
+ selftext = ::File.read(__FILE__)
10
+ ::File.open(__FILE__, 'r') do |io|
11
+ assert_equal(selftext, Input.new(io).string)
12
+ end
13
+
14
+ # to_path
15
+ path = Struct.new(:to_path).new(__FILE__)
16
+ assert_equal(selftext, Input.new(path).string)
17
+ end
18
+
4
19
  def test_memoized?
5
20
  assert_equal(false, Input.new('').memoized?)
6
21
  end
@@ -20,6 +20,23 @@ class MatchTest < Test::Unit::TestCase
20
20
  assert_equal(false, match2 == match1)
21
21
  end
22
22
 
23
+ def test_source
24
+ match1 = Match.new('abcdef')
25
+ assert_equal 'abcdef', match1.source
26
+
27
+ path = Struct.new(:to_path).new(__FILE__)
28
+ match2 = Match.new(Input.new(path))
29
+ assert_equal path, match2.source
30
+ end
31
+
32
+ def test_string
33
+ match1 = Match.new('abcdef')
34
+ assert_equal 'abcdef', match1.string
35
+
36
+ match2 = Match.new('abcdef', [Rule.for('bcd'), -1, 3], 1)
37
+ assert_equal 'bcd', match2.string
38
+ end
39
+
23
40
  def test_matches
24
41
  a = Rule.for('a')
25
42
  b = Rule.for('b')
@@ -59,11 +76,18 @@ class MatchTest < Test::Unit::TestCase
59
76
  CLOSE, 3
60
77
  ]
61
78
 
62
- match.matches.each do |m|
79
+ match.matches.each_with_index do |m, i|
63
80
  assert_equal(sub_events, m.events)
81
+ assert_equal(i*3, m.offset)
82
+ assert_equal(3, m.length)
83
+ assert_equal("abc", m.string)
64
84
  assert_equal("abc", m)
65
85
  assert(m.matches)
66
86
  assert_equal(3, m.matches.length)
87
+ m.matches.each_with_index do |m2,i2|
88
+ assert_equal(i*3+i2, m2.offset)
89
+ assert_equal(1, m2.length)
90
+ end
67
91
  end
68
92
  end
69
93
 
@@ -18,7 +18,6 @@ class SuperTest < Test::Unit::TestCase
18
18
  rule_2a = grammar2.rule(:a)
19
19
  rule_2a_als = rule_2a.rules[0]
20
20
  rule_2a_sup = rule_2a.rules[1]
21
- rule_1a = grammar1.rule(:a)
22
21
 
23
22
  events = rule_2a.exec(Input.new('abc'))
24
23
  assert_equal([
@@ -58,12 +57,9 @@ class SuperTest < Test::Unit::TestCase
58
57
  rule :a, any(sup, :b)
59
58
  rule :b, sup
60
59
  }
61
- rule_1a = grammar1.rule(:a)
62
- rule_1b = grammar1.rule(:b)
63
60
  rule_2a = grammar2.rule(:a)
64
61
  rule_2a_sup = rule_2a.rules[0]
65
62
  rule_2a_als = rule_2a.rules[1]
66
- rule_2b = grammar2.rule(:b)
67
63
 
68
64
  events = rule_2a.exec(Input.new('abc'))
69
65
  assert_equal([
metadata CHANGED
@@ -1,47 +1,40 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: citrus
3
- version: !ruby/object:Gem::Version
4
- hash: 29
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.5.0
5
5
  prerelease:
6
- segments:
7
- - 2
8
- - 4
9
- - 1
10
- version: 2.4.1
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Michael Jackson
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2011-11-04 00:00:00 -07:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
12
+ date: 2014-03-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
22
15
  name: rake
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
25
17
  none: false
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- hash: 3
30
- segments:
31
- - 0
32
- version: "0"
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
33
22
  type: :development
34
- version_requirements: *id001
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
35
30
  description: Parsing Expressions for Ruby
36
31
  email: mjijackson@gmail.com
37
32
  executables: []
38
-
39
33
  extensions: []
40
-
41
- extra_rdoc_files:
34
+ extra_rdoc_files:
42
35
  - README.md
43
36
  - CHANGES
44
- files:
37
+ files:
45
38
  - benchmark/seqpar.citrus
46
39
  - benchmark/seqpar.gnuplot
47
40
  - benchmark/seqpar.rb
@@ -69,6 +62,8 @@ files:
69
62
  - test/_files/rule3.citrus
70
63
  - test/_files/rule4.citrus
71
64
  - test/_files/rule5.citrus
65
+ - test/_files/rule6.citrus
66
+ - test/_files/rule7.citrus
72
67
  - test/alias_test.rb
73
68
  - test/and_predicate_test.rb
74
69
  - test/but_predicate_test.rb
@@ -99,46 +94,37 @@ files:
99
94
  - Rakefile
100
95
  - README.md
101
96
  - CHANGES
102
- has_rdoc: true
103
97
  homepage: http://mjijackson.com/citrus
104
98
  licenses: []
105
-
106
99
  post_install_message:
107
- rdoc_options:
100
+ rdoc_options:
108
101
  - --line-numbers
109
102
  - --inline-source
110
103
  - --title
111
104
  - Citrus
112
105
  - --main
113
106
  - Citrus
114
- require_paths:
107
+ require_paths:
115
108
  - lib
116
- required_ruby_version: !ruby/object:Gem::Requirement
109
+ required_ruby_version: !ruby/object:Gem::Requirement
117
110
  none: false
118
- requirements:
119
- - - ">="
120
- - !ruby/object:Gem::Version
121
- hash: 3
122
- segments:
123
- - 0
124
- version: "0"
125
- required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ! '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
116
  none: false
127
- requirements:
128
- - - ">="
129
- - !ruby/object:Gem::Version
130
- hash: 3
131
- segments:
132
- - 0
133
- version: "0"
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
134
121
  requirements: []
135
-
136
122
  rubyforge_project:
137
- rubygems_version: 1.6.2
123
+ rubygems_version: 1.8.23
138
124
  signing_key:
139
125
  specification_version: 3
140
126
  summary: Parsing Expressions for Ruby
141
- test_files:
127
+ test_files:
142
128
  - test/alias_test.rb
143
129
  - test/and_predicate_test.rb
144
130
  - test/but_predicate_test.rb