citrus 2.4.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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