regexp_parser 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dedba6b051b22dd917febd957e35e6800b37af958780e331007de1f59b2b466
4
- data.tar.gz: 55fa98afa6031a38cac2045f8c592290fad9a4e500597277f3f79be75f1076f3
3
+ metadata.gz: 8b84a4bb274f31b8608c7dc9d55ff6f1b8d92d0d147976f38079ae7701a6debe
4
+ data.tar.gz: 41db5f094d0beafade30a1fac2707cbc827831e818c485ad35d7173f18c6a91a
5
5
  SHA512:
6
- metadata.gz: 6b2b463f3a28450527691d90bcfc8901b815bd4a32e88bcdee1f95db4588599993766687a4c7a1b785b450c0f0025039caf07bb93aaf1013fa365e4ed16fc040
7
- data.tar.gz: 8aabccda06bb1f20485610076ad679a0d62e8630f992c55be7158c01f3b8b6dd6eb44a206f71b613c0335db13789d8e21d652973ecfb7c262c25a6d54e35a371
6
+ metadata.gz: 5dcde6135ac42db609402e47e04ee3be1da8854de286d2baad15dafee04d451814fd7a3bae7adc5440a1fced811e242b69f5fd14bcfc4f3bd5091f86769d56be
7
+ data.tar.gz: 2660d0fb28a972a1de53b71b16f8591e573d4214724b5eea8a452549598ff5d0fc5b731149e8332f65bce01c812f4d0d72135bba7e3016064d9f05202a8b5580
data/CHANGELOG.md CHANGED
@@ -1,11 +1,54 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.4.0] - 2022-05-09 - [Janosch Müller](mailto:janosch84@gmail.com)
4
+
5
+ ### Fixed
6
+
7
+ - fixed interpretation of `+` and `?` after interval quantifiers (`{n,n}`)
8
+ - they used to be treated as reluctant or possessive mode indicators
9
+ - however, Ruby does not support these modes for interval quantifiers
10
+ - they are now treated as chained quantifiers instead, as Ruby does it
11
+ - c.f. [#3](https://github.com/ammar/regexp_parser/issues/3)
12
+ - fixed `Expression::Base#nesting_level` for some tree rewrite cases
13
+ - e.g. the alternatives in `/a|[b]/` had an inconsistent nesting_level
14
+ - fixed `Scanner` accepting invalid posix classes, e.g. `[[:foo:]]`
15
+ - they raise a `SyntaxError` when used in a Regexp, so could only be passed as String
16
+ - they now raise a `Regexp::Scanner::ValidationError` in the `Scanner`
17
+
18
+ ### Added
19
+
20
+ - added `Expression::Base#==` for (deep) comparison of expressions
21
+ - added `Expression::Base#parts`
22
+ - returns the text elements and subexpressions of an expression
23
+ - e.g. `parse(/(a)/)[0].parts # => ["(", #<Literal @text="a"...>, ")"]`
24
+ - added `Expression::Base#te` (a.k.a. token end index)
25
+ - `Expression::Subexpression` always had `#te`, only terminal nodes lacked it so far
26
+ - made some `Expression::Base` methods available on `Quantifier` instances, too
27
+ - `#type`, `#type?`, `#is?`, `#one_of?`, `#options`, `#terminal?`
28
+ - `#base_length`, `#full_length`, `#starts_at`, `#te`, `#ts`, `#offset`
29
+ - `#conditional_level`, `#level`, `#nesting_level` , `#set_level`
30
+ - this allows a more unified handling with `Expression::Base` instances
31
+ - allowed `Quantifier#initialize` to take a token and options Hash like other nodes
32
+ - added a deprecation warning for initializing Quantifiers with 4+ arguments:
33
+
34
+ Calling `Expression::Base#quantify` or `Quantifier.new` with 4+ arguments
35
+ is deprecated.
36
+
37
+ It will no longer be supported in regexp_parser v3.0.0.
38
+
39
+ Please pass a Regexp::Token instead, e.g. replace `type, text, min, max, mode`
40
+ with `::Regexp::Token.new(:quantifier, type, text)`. min, max, and mode
41
+ will be derived automatically.
42
+
43
+ This is consistent with how Expression::Base instances are created.
44
+
45
+
3
46
  ## [2.3.1] - 2022-04-24 - [Janosch Müller](mailto:janosch84@gmail.com)
4
47
 
5
48
  ### Fixed
6
49
 
7
50
  - removed five inexistent unicode properties from `Syntax#features`
8
- - these were never supported by Ruby but incorrectly accepted by the parser
51
+ - these were never supported by Ruby or the `Regexp::Scanner`
9
52
  - thanks to [Markus Schirp](https://github.com/mbj) for the report
10
53
 
11
54
  ## [2.3.0] - 2022-04-08 - [Janosch Müller](mailto:janosch84@gmail.com)
@@ -188,7 +231,7 @@
188
231
 
189
232
  ### Added
190
233
 
191
- - `Expression#each_expression` and `#traverse` can now be called without a block
234
+ - `Expression::Base#each_expression` and `#traverse` can now be called without a block
192
235
  * this returns an `Enumerator` and allows chaining, e.g. `each_expression.select`
193
236
  * thanks to [Masataka Kuwabara](https://github.com/pocke)
194
237
 
@@ -214,7 +257,7 @@
214
257
  - Fixed `Group#option_changes` not accounting for indirectly disabled (overridden) encoding flags
215
258
  - Fixed `Scanner` allowing negative encoding options if there were no positive options, e.g. '(?-u)'
216
259
  - Fixed `ScannerError` for some valid meta/control sequences such as '\\C-\\\\'
217
- - Fixed `Expression#match` and `#=~` not working with a single argument
260
+ - Fixed `Expression::Base#match` and `#=~` not working with a single argument
218
261
 
219
262
  ### [1.5.0] - 2019-05-14 - [Janosch Müller](mailto:janosch84@gmail.com)
220
263
 
@@ -222,15 +265,15 @@
222
265
 
223
266
  - Added `#referenced_expression` for backrefs, subexp calls and conditionals
224
267
  * returns the `Group` expression that is being referenced via name or number
225
- - Added `Expression#repetitions`
268
+ - Added `Expression::Base#repetitions`
226
269
  * returns a `Range` of allowed repetitions (`1..1` if there is no quantifier)
227
270
  * like `#quantity` but with a more uniform interface
228
- - Added `Expression#match_length`
271
+ - Added `Expression::Base#match_length`
229
272
  * allows to inspect and iterate over String lengths matched by the Expression
230
273
 
231
274
  ### Fixed
232
275
 
233
- - Fixed `Expression#clone` "direction"
276
+ - Fixed `Expression::Base#clone` "direction"
234
277
  * it used to dup ivars onto the callee, leaving only the clone referencing the original objects
235
278
  * this will affect you if you call `#eql?`/`#equal?` on expressions or use them as Hash keys
236
279
  - Fixed `#clone` results for `Sequences`, e.g. alternations and conditionals
@@ -392,7 +435,7 @@ This release includes several breaking changes, mostly to character sets, #map a
392
435
  - Fixed a thread safety issue (issue #45)
393
436
  - Some public class methods that were only reliable for
394
437
  internal use are now private instance methods (PR #46)
395
- - Improved the usefulness of Expression#options (issue #43) -
438
+ - Improved the usefulness of Expression::Base#options (issue #43) -
396
439
  #options and derived methods such as #i?, #m? and #x? are now
397
440
  defined for all Expressions that are affected by such flags.
398
441
  - Fixed scanning of whitespace following (?x) (commit 5c94bd2)
data/README.md CHANGED
@@ -367,12 +367,12 @@ _Note that not all of these are available in all versions of Ruby_
367
367
  | **POSIX Classes** | `[:alpha:]`, `[:^digit:]` | &#x2713; |
368
368
  | **Quantifiers** | | &#x22f1; |
369
369
  | &emsp;&nbsp;_**Greedy**_ | `?`, `*`, `+`, `{m,M}` | &#x2713; |
370
- | &emsp;&nbsp;_**Reluctant** (Lazy)_ | `??`, `*?`, `+?`, `{m,M}?` | &#x2713; |
371
- | &emsp;&nbsp;_**Possessive**_ | `?+`, `*+`, `++`, `{m,M}+` | &#x2713; |
370
+ | &emsp;&nbsp;_**Reluctant** (Lazy)_ | `??`, `*?`, `+?` \[1\] | &#x2713; |
371
+ | &emsp;&nbsp;_**Possessive**_ | `?+`, `*+`, `++` \[1\] | &#x2713; |
372
372
  | **String Escapes** | | &#x22f1; |
373
- | &emsp;&nbsp;_**Control** \[1\]_ | `\C-C`, `\cD` | &#x2713; |
373
+ | &emsp;&nbsp;_**Control** \[2\]_ | `\C-C`, `\cD` | &#x2713; |
374
374
  | &emsp;&nbsp;_**Hex**_ | `\x20`, `\x{701230}` | &#x2713; |
375
- | &emsp;&nbsp;_**Meta** \[1\]_ | `\M-c`, `\M-\C-C`, `\M-\cC`, `\C-\M-C`, `\c\M-C` | &#x2713; |
375
+ | &emsp;&nbsp;_**Meta** \[2\]_ | `\M-c`, `\M-\C-C`, `\M-\cC`, `\C-\M-C`, `\c\M-C` | &#x2713; |
376
376
  | &emsp;&nbsp;_**Octal**_ | `\0`, `\01`, `\012` | &#x2713; |
377
377
  | &emsp;&nbsp;_**Unicode**_ | `\uHHHH`, `\u{H+ H+}` | &#x2713; |
378
378
  | **Unicode Properties** | _<sub>([Unicode 13.0.0](https://www.unicode.org/versions/Unicode13.0.0/))</sub>_ | &#x22f1; |
@@ -384,7 +384,11 @@ _Note that not all of these are available in all versions of Ruby_
384
384
  | &emsp;&nbsp;_**Scripts**_ | `\p{Arabic}`, `\P{Hiragana}`, `\p{^Greek}` | &#x2713; |
385
385
  | &emsp;&nbsp;_**Simple**_ | `\p{Dash}`, `\p{Extender}`, `\p{^Hyphen}` | &#x2713; |
386
386
 
387
- **\[1\]**: As of Ruby 3.1, meta and control sequences are [pre-processed to hex escapes when used in Regexp literals](
387
+ **\[1\]**: Ruby does not support lazy or possessive interval quantifiers. Any `+` or `?` that follows an interval
388
+ quantifier will be treated as another, chained quantifier. See also [#3](https://github.com/ammar/regexp_parser/issue/3),
389
+ [#69](https://github.com/ammar/regexp_parser/pull/69).
390
+
391
+ **\[2\]**: As of Ruby 3.1, meta and control sequences are [pre-processed to hex escapes when used in Regexp literals](
388
392
  https://github.com/ruby/ruby/commit/11ae581a4a7f5d5f5ec6378872eab8f25381b1b9 ), so they will only reach the
389
393
  scanner and will only be emitted if a String or a Regexp that has been built with the `::new` constructor is scanned.
390
394
 
@@ -1,4 +1,4 @@
1
1
  class Regexp::Parser
2
- # base class for all gem-specific errors (inherited but never raised itself)
2
+ # base class for all gem-specific errors
3
3
  class Error < StandardError; end
4
4
  end
@@ -1,29 +1,15 @@
1
1
  module Regexp::Expression
2
2
  class Base
3
- attr_accessor :type, :token
4
- attr_accessor :text, :ts
5
- attr_accessor :level, :set_level, :conditional_level, :nesting_level
6
-
7
- attr_accessor :quantifier
8
- attr_accessor :options
3
+ include Regexp::Expression::Shared
9
4
 
10
5
  def initialize(token, options = {})
11
- self.type = token.type
12
- self.token = token.token
13
- self.text = token.text
14
- self.ts = token.ts
15
- self.level = token.level
16
- self.set_level = token.set_level
17
- self.conditional_level = token.conditional_level
18
- self.nesting_level = 0
19
- self.quantifier = nil
20
- self.options = options
6
+ init_from_token_and_options(token, options)
21
7
  end
22
8
 
23
9
  def initialize_copy(orig)
24
- self.text = (orig.text ? orig.text.dup : nil)
25
- self.options = (orig.options ? orig.options.dup : nil)
26
- self.quantifier = (orig.quantifier ? orig.quantifier.clone : nil)
10
+ self.text = orig.text.dup if orig.text
11
+ self.options = orig.options.dup if orig.options
12
+ self.quantifier = orig.quantifier.clone if orig.quantifier
27
13
  super
28
14
  end
29
15
 
@@ -31,48 +17,14 @@ module Regexp::Expression
31
17
  ::Regexp.new(to_s(format))
32
18
  end
33
19
 
34
- alias :starts_at :ts
35
-
36
- def base_length
37
- to_s(:base).length
38
- end
39
-
40
- def full_length
41
- to_s.length
42
- end
43
-
44
- def offset
45
- [starts_at, full_length]
46
- end
47
-
48
- def coded_offset
49
- '@%d+%d' % offset
50
- end
51
-
52
- def to_s(format = :full)
53
- "#{text}#{quantifier_affix(format)}"
54
- end
55
-
56
- def quantifier_affix(expression_format)
57
- quantifier.to_s if quantified? && expression_format != :base
58
- end
59
-
60
- def terminal?
61
- !respond_to?(:expressions)
62
- end
63
-
64
- def quantify(token, text, min = nil, max = nil, mode = :greedy)
65
- self.quantifier = Quantifier.new(token, text, min, max, mode)
20
+ def quantify(*args)
21
+ self.quantifier = Quantifier.new(*args)
66
22
  end
67
23
 
68
24
  def unquantified_clone
69
25
  clone.tap { |exp| exp.quantifier = nil }
70
26
  end
71
27
 
72
- def quantified?
73
- !quantifier.nil?
74
- end
75
-
76
28
  # Deprecated. Prefer `#repetitions` which has a more uniform interface.
77
29
  def quantity
78
30
  return [nil,nil] unless quantified?
@@ -104,7 +56,7 @@ module Regexp::Expression
104
56
  quantified? and quantifier.possessive?
105
57
  end
106
58
 
107
- def attributes
59
+ def to_h
108
60
  {
109
61
  type: type,
110
62
  token: token,
@@ -118,6 +70,6 @@ module Regexp::Expression
118
70
  quantifier: quantified? ? quantifier.to_h : nil,
119
71
  }
120
72
  end
121
- alias :to_h :attributes
73
+ alias :attributes :to_h
122
74
  end
123
75
  end
@@ -16,8 +16,8 @@ module Regexp::Expression
16
16
  count == 2
17
17
  end
18
18
 
19
- def to_s(_format = :full)
20
- expressions.join(text)
19
+ def parts
20
+ intersperse(expressions, text.dup)
21
21
  end
22
22
  end
23
23
  end
@@ -20,8 +20,8 @@ module Regexp::Expression
20
20
  self.closed = true
21
21
  end
22
22
 
23
- def to_s(format = :full)
24
- "#{text}#{'^' if negated?}#{expressions.join}]#{quantifier_affix(format)}"
23
+ def parts
24
+ ["#{text}#{'^' if negated?}", *expressions, ']']
25
25
  end
26
26
  end
27
27
  end # module Regexp::Expression
@@ -55,8 +55,8 @@ module Regexp::Expression
55
55
  condition.reference
56
56
  end
57
57
 
58
- def to_s(format = :full)
59
- "#{text}#{condition}#{branches.join('|')})#{quantifier_affix(format)}"
58
+ def parts
59
+ [text.dup, condition, *intersperse(branches, '|'), ')']
60
60
  end
61
61
 
62
62
  def initialize_copy(orig)
@@ -1,6 +1,6 @@
1
1
  module Regexp::Expression
2
2
  class FreeSpace < Regexp::Expression::Base
3
- def quantify(_token, _text, _min = nil, _max = nil, _mode = :greedy)
3
+ def quantify(*_args)
4
4
  raise Regexp::Parser::Error, 'Can not quantify a free space object'
5
5
  end
6
6
  end
@@ -1,8 +1,8 @@
1
1
  module Regexp::Expression
2
2
  module Group
3
3
  class Base < Regexp::Expression::Subexpression
4
- def to_s(format = :full)
5
- "#{text}#{expressions.join})#{quantifier_affix(format)}"
4
+ def parts
5
+ [text.dup, *expressions, ')']
6
6
  end
7
7
 
8
8
  def capturing?; false end
@@ -18,9 +18,9 @@ module Regexp::Expression
18
18
  super
19
19
  end
20
20
 
21
- def to_s(format = :full)
21
+ def parts
22
22
  if implicit?
23
- "#{expressions.join}#{quantifier_affix(format)}"
23
+ expressions
24
24
  else
25
25
  super
26
26
  end
@@ -65,8 +65,8 @@ module Regexp::Expression
65
65
  end
66
66
 
67
67
  class Comment < Group::Base
68
- def to_s(_format = :full)
69
- text.dup
68
+ def parts
69
+ [text.dup]
70
70
  end
71
71
 
72
72
  def comment?; true end
@@ -1,5 +1,5 @@
1
1
  module Regexp::Expression
2
- class Base
2
+ module Shared
3
3
 
4
4
  # Test if this expression has the given test_type, which can be either
5
5
  # a symbol or an array of symbols to check against the expression's type.
@@ -93,5 +93,14 @@ module Regexp::Expression
93
93
  "Array, Hash, or Symbol expected, #{scope.class.name} given"
94
94
  end
95
95
  end
96
+
97
+ # Deep-compare two expressions for equality.
98
+ def ==(other)
99
+ other.class == self.class &&
100
+ other.to_s == to_s &&
101
+ other.options == options
102
+ end
103
+ alias :=== :==
104
+ alias :eql? :==
96
105
  end
97
106
  end
@@ -1,26 +1,24 @@
1
1
  module Regexp::Expression
2
+ # TODO: in v3.0.0, maybe put Shared back into Base, and inherit from Base and
3
+ # call super in #initialize, but raise in #quantifier= and #quantify,
4
+ # or introduce an Expression::Quantifiable intermediate class.
5
+ # Or actually allow chaining as a more concise but tricky solution than PR#69.
2
6
  class Quantifier
7
+ include Regexp::Expression::Shared
8
+
3
9
  MODES = %i[greedy possessive reluctant]
4
10
 
5
- attr_reader :token, :text, :min, :max, :mode
11
+ attr_reader :min, :max, :mode
6
12
 
7
- def initialize(token, text, min, max, mode)
8
- @token = token
9
- @text = text
10
- @mode = mode
11
- @min = min
12
- @max = max
13
- end
14
-
15
- def initialize_copy(orig)
16
- @text = orig.text.dup
17
- super
18
- end
13
+ def initialize(*args)
14
+ deprecated_old_init(*args) and return if args.count == 4 || args.count == 5
19
15
 
20
- def to_s
21
- text.dup
16
+ init_from_token_and_options(*args)
17
+ @mode = (token[/greedy|reluctant|possessive/] || :greedy).to_sym
18
+ @min, @max = minmax
19
+ # TODO: remove in v3.0.0, stop removing parts of #token (?)
20
+ self.token = token.to_s.sub(/_(greedy|possessive|reluctant)/, '').to_sym
22
21
  end
23
- alias :to_str :to_s
24
22
 
25
23
  def to_h
26
24
  {
@@ -41,13 +39,32 @@ module Regexp::Expression
41
39
  end
42
40
  alias :lazy? :reluctant?
43
41
 
44
- def ==(other)
45
- other.class == self.class &&
46
- other.token == token &&
47
- other.mode == mode &&
48
- other.min == min &&
49
- other.max == max
42
+ private
43
+
44
+ def deprecated_old_init(token, text, min, max, mode = :greedy)
45
+ warn "Calling `Expression::Base#quantify` or `#{self.class}.new` with 4+ arguments "\
46
+ "is deprecated.\nIt will no longer be supported in regexp_parser v3.0.0.\n"\
47
+ "Please pass a Regexp::Token instead, e.g. replace `type, text, min, max, mode` "\
48
+ "with `::Regexp::Token.new(:quantifier, type, text)`. min, max, and mode "\
49
+ "will be derived automatically. \nThis is consistent with how Expression::Base "\
50
+ "instances are created."
51
+ @token = token
52
+ @text = text
53
+ @min = min
54
+ @max = max
55
+ @mode = mode
56
+ end
57
+
58
+ def minmax
59
+ case token
60
+ when /zero_or_one/ then [0, 1]
61
+ when /zero_or_more/ then [0, -1]
62
+ when /one_or_more/ then [1, -1]
63
+ when :interval
64
+ int_min = text[/\{(\d*)/, 1]
65
+ int_max = text[/,?(\d*)\}/, 1]
66
+ [int_min.to_i, (int_max.empty? ? -1 : int_max.to_i)]
67
+ end
50
68
  end
51
- alias :eq :==
52
69
  end
53
70
  end
@@ -39,12 +39,12 @@ module Regexp::Expression
39
39
  end
40
40
  alias :ts :starts_at
41
41
 
42
- def quantify(token, text, min = nil, max = nil, mode = :greedy)
42
+ def quantify(*args)
43
43
  target = expressions.reverse.find { |exp| !exp.is_a?(FreeSpace) }
44
44
  target or raise Regexp::Parser::Error,
45
45
  "No valid target found for '#{text}' quantifier"
46
46
 
47
- target.quantify(token, text, min, max, mode)
47
+ target.quantify(*args)
48
48
  end
49
49
  end
50
50
  end
@@ -18,8 +18,8 @@ module Regexp::Expression
18
18
  self.class::OPERAND.add_to(self, {}, active_opts)
19
19
  end
20
20
 
21
- def to_s(format = :full)
22
- sequences.map { |e| e.to_s(format) }.join(text)
21
+ def parts
22
+ intersperse(expressions, text.dup)
23
23
  end
24
24
  end
25
25
  end
@@ -0,0 +1,81 @@
1
+ module Regexp::Expression
2
+ module Shared
3
+ def self.included(mod)
4
+ mod.class_eval do
5
+ attr_accessor :type, :token, :text, :ts, :te,
6
+ :level, :set_level, :conditional_level,
7
+ :options, :quantifier
8
+
9
+ attr_reader :nesting_level
10
+ end
11
+ end
12
+
13
+ def init_from_token_and_options(token, options = {})
14
+ self.type = token.type
15
+ self.token = token.token
16
+ self.text = token.text
17
+ self.ts = token.ts
18
+ self.te = token.te
19
+ self.level = token.level
20
+ self.set_level = token.set_level
21
+ self.conditional_level = token.conditional_level
22
+ self.nesting_level = 0
23
+ self.options = options || {}
24
+ end
25
+ private :init_from_token_and_options
26
+
27
+ def initialize_copy(orig)
28
+ self.text = orig.text.dup if orig.text
29
+ self.options = orig.options.dup if orig.options
30
+ self.quantifier = orig.quantifier.clone if orig.quantifier
31
+ super
32
+ end
33
+
34
+ def starts_at
35
+ ts
36
+ end
37
+
38
+ def base_length
39
+ to_s(:base).length
40
+ end
41
+
42
+ def full_length
43
+ to_s.length
44
+ end
45
+
46
+ def to_s(format = :full)
47
+ "#{parts.join}#{quantifier_affix(format)}"
48
+ end
49
+ alias :to_str :to_s
50
+
51
+ def parts
52
+ [text.dup]
53
+ end
54
+
55
+ def quantifier_affix(expression_format)
56
+ quantifier.to_s if quantified? && expression_format != :base
57
+ end
58
+
59
+ def quantified?
60
+ !quantifier.nil?
61
+ end
62
+
63
+ def offset
64
+ [starts_at, full_length]
65
+ end
66
+
67
+ def coded_offset
68
+ '@%d+%d' % offset
69
+ end
70
+
71
+ def terminal?
72
+ !respond_to?(:expressions)
73
+ end
74
+
75
+ def nesting_level=(lvl)
76
+ @nesting_level = lvl
77
+ quantifier && quantifier.nesting_level = lvl
78
+ terminal? || each { |subexp| subexp.nesting_level = lvl + 1 }
79
+ end
80
+ end
81
+ end
@@ -5,9 +5,8 @@ module Regexp::Expression
5
5
  attr_accessor :expressions
6
6
 
7
7
  def initialize(token, options = {})
8
- super
9
-
10
8
  self.expressions = []
9
+ super
11
10
  end
12
11
 
13
12
  # Override base method to clone the expressions as well.
@@ -43,16 +42,21 @@ module Regexp::Expression
43
42
  ts + to_s.length
44
43
  end
45
44
 
46
- def to_s(format = :full)
47
- # Note: the format does not get passed down to subexpressions.
48
- "#{expressions.join}#{quantifier_affix(format)}"
45
+ def parts
46
+ expressions
49
47
  end
50
48
 
51
49
  def to_h
52
- attributes.merge({
50
+ attributes.merge(
53
51
  text: to_s(:base),
54
52
  expressions: expressions.map(&:to_h)
55
- })
53
+ )
54
+ end
55
+
56
+ private
57
+
58
+ def intersperse(expressions, separator)
59
+ expressions.flat_map { |exp| [exp, separator] }.slice(0...-1)
56
60
  end
57
61
  end
58
62
  end
@@ -1,5 +1,6 @@
1
1
  require 'regexp_parser/error'
2
2
 
3
+ require 'regexp_parser/expression/shared'
3
4
  require 'regexp_parser/expression/base'
4
5
  require 'regexp_parser/expression/quantifier'
5
6
  require 'regexp_parser/expression/subexpression'