fop_lang 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5f19a543b81c0046dc63fcc1c0769989628017d2c1d1da74ef0db9866a0f2f7
4
- data.tar.gz: 03b6597f9cab97c95ccda8396693bb43d9da729137cb916cc74f7fbecc314b32
3
+ metadata.gz: 8ed95bb4708820a186e6485cc29dbb47286b0f309a1caf91af7778d768b0efb3
4
+ data.tar.gz: 85d41728ddae13f3667f0a2d55a5c4dcbc26e8217d4f31466e6ed92038859881
5
5
  SHA512:
6
- metadata.gz: 3a17c82a561e20cbc5cb8abbad5be4f94f02110d60b6130e3e1e9489672c5c134befc6b1daca2f590f083a67934e600fb5d6fa0ea5433181ba3014514c558232
7
- data.tar.gz: 790250c8a79dcf04b381f2dd33cbaa048fd070688ab45446ff87652dcb18844c2d6139d0ead060fa338a57b8590eee0167ea2c25abd84e1d71571f33c49bcbda
6
+ metadata.gz: e650bdf66d8d0b5dcb603eae494f38d4969a19647053b4e81e0612705f5b16a5755d007e4ce01fad9b487224d66eff462738f1e37b011ba2a4bf4a45b0203bb3
7
+ data.tar.gz: 99b31736236785cecc85b9bb23ccc3e366713cd23dd04c992a8fff6676a316d5741bfe5058e17cd0812c43d8bf1979aa7546184386018c1f92ed2462a85eb5fb
data/README.md CHANGED
@@ -29,7 +29,7 @@ The above expression contains the only two parts of Fop (except for the wildcard
29
29
 
30
30
  **Text Literals**
31
31
 
32
- A text literal works how it sounds: the input must match it exactly. The only exception is the `*` (wildcard) character, which matches 0 or more of anything. Wildcards can be used anywhere except inside `{...}` (operations).
32
+ A text literal works how it sounds: the input must match it exactly. If it matches it passes through unchanged. The only exception is the `*` (wildcard) character, which matches 0 or more of anything. Wildcards can be used anywhere except inside `{...}` (operations).
33
33
 
34
34
  If `\` (escape) is used before the special characters `*`, `{` or `}`, then that character is treated like a text literal. It's recommended to use single-quoted Ruby strings with Fop expressions that so you don't need to double-escape.
35
35
 
@@ -42,9 +42,11 @@ Operations are the interesting part of Fop, and are specified between `{` and `}
42
42
  * `A` is the alpha class and will match one or more letters (lower or upper case).
43
43
  * `W` is the word class and matches alphanumeric chars and underscores.
44
44
  * `*` is the wildcard class and greedily matches everything after it.
45
- * `/.../` matches on the supplied regex between the `/`'s. If you're regex contains a `/`, it must be escaped.
45
+ * `/.../` matches on the supplied regex between the `/`'s. If you're regex contains a `/`, it must be escaped. Capture groups may be referenced in the operator argument as `$1`, `$2`, etc.
46
46
  3. Operator (optional): What to do to the matching characters.
47
- * `=` Replace the matching character(s) with the given argument. If no argument is given, drop the matching chars. Note that any `/` chars must be escaped, so as not to be mistaken for a regex.
47
+ * `=` Replace the matching character(s) with the given argument. If no argument is given, drop the matching chars.
48
+ * `>` Append the following chars to the matching value.
49
+ * `<` Prepend the following chars to the matching value.
48
50
  * `+` Perform addition on the matching number and the argument (`N` only).
49
51
  * `-` Subtract the argument from the matching number (`N` only).
50
52
  5. Operator argument (required for some operators): meaning varies by operator.
data/lib/fop/nodes.rb CHANGED
@@ -12,11 +12,15 @@ module Fop
12
12
  end
13
13
  end
14
14
 
15
- Op = Struct.new(:wildcard, :match, :regex_match, :regex, :operator, :operator_arg, :expression) do
15
+ Op = Struct.new(:wildcard, :match, :regex_match, :regex, :operator, :operator_arg, :operator_arg_w_caps, :expression) do
16
16
  def consume!(input)
17
- if (val = input.slice!(regex))
18
- found_val = regex_match || val != Parser::BLANK
19
- expression && found_val ? expression.call(val) : val
17
+ if (match = regex.match(input))
18
+ val = match.to_s
19
+ blank = val == Parser::BLANK
20
+ input.sub!(val, Parser::BLANK) unless blank
21
+ found_val = regex_match || !blank
22
+ arg = operator_arg_w_caps ? sub_caps(operator_arg_w_caps, match.captures) : operator_arg
23
+ expression && found_val ? expression.call(val, operator, arg) : val
20
24
  end
21
25
  end
22
26
 
@@ -26,6 +30,18 @@ module Fop
26
30
  s << " #{operator} #{operator_arg}" if operator
27
31
  s
28
32
  end
33
+
34
+ private
35
+
36
+ def sub_caps(tokens, caps)
37
+ tokens.map { |t|
38
+ case t
39
+ when String then t
40
+ when Parser::CaptureGroup then caps[t.index].to_s
41
+ else raise Parser::Error, "Unexpected #{t} in capture group"
42
+ end
43
+ }.join("")
44
+ end
29
45
  end
30
46
  end
31
47
  end
data/lib/fop/parser.rb CHANGED
@@ -3,6 +3,7 @@ require_relative 'nodes'
3
3
  module Fop
4
4
  module Parser
5
5
  Error = Class.new(StandardError)
6
+ CaptureGroup = Struct.new(:index)
6
7
 
7
8
  MATCH_NUM = "N".freeze
8
9
  MATCH_WORD = "W".freeze
@@ -10,10 +11,19 @@ module Fop
10
11
  MATCH_WILD = "*".freeze
11
12
  BLANK = "".freeze
12
13
  OP_REPLACE = "=".freeze
14
+ OP_APPEND = ">".freeze
15
+ OP_PREPEND = "<".freeze
13
16
  OP_ADD = "+".freeze
14
17
  OP_SUB = "-".freeze
15
18
  OP_MUL = "*".freeze
16
19
  OP_DIV = "/".freeze
20
+ VAR = "$".freeze
21
+ CAP_NUM = /^[1-9]$/
22
+
23
+ EXP_REPLACE = ->(_val, _op, arg) { arg || BLANK }
24
+ EXP_MATH = ->(val, op, arg) { val.to_i.send(op, arg.to_i) }
25
+ EXP_APPEND = ->(val, _op, arg) { val + arg }
26
+ EXP_PREPEND = ->(val, _op, arg) { arg + val }
17
27
 
18
28
  def self.parse!(tokens)
19
29
  nodes = []
@@ -45,7 +55,7 @@ module Fop
45
55
  when Nodes::Text, Nodes::Op
46
56
  nodes << curr_node
47
57
  else
48
- raise "Unexpected end node #{curr_node}"
58
+ raise Error, "Unexpected end node #{curr_node}"
49
59
  end
50
60
 
51
61
  nodes
@@ -59,7 +69,7 @@ module Fop
59
69
  Nodes::Text.new(wildcard, token.char.clone)
60
70
  when Tokenizer::Op
61
71
  op = Nodes::Op.new(wildcard)
62
- parse_op! op, token.tokens
72
+ parse_op! op, token
63
73
  op
64
74
  when :wildcard
65
75
  :wildcard
@@ -85,52 +95,87 @@ module Fop
85
95
  end
86
96
  end
87
97
 
88
- def self.parse_op!(node, tokens)
89
- t = tokens[0] || raise(Error, "Empty operation")
98
+ def self.parse_op!(node, token)
90
99
  # parse the matching type
91
100
  node.regex =
92
- case t
101
+ case token.match
93
102
  when Tokenizer::Char
94
- node.match = t.char
103
+ node.match = token.match.char
95
104
  node.regex_match = false
96
- case t.char
105
+ case node.match
97
106
  when MATCH_NUM then Regexp.new((node.wildcard ? ".*?" : "^") + "[0-9]+")
98
107
  when MATCH_WORD then Regexp.new((node.wildcard ? ".*?" : "^") + "\\w+")
99
108
  when MATCH_ALPHA then Regexp.new((node.wildcard ? ".*?" : "^") + "[a-zA-Z]+")
100
109
  when MATCH_WILD then /.*/
101
- else raise Error, "Unknown match type '#{t.char}'"
110
+ else raise Error, "Unknown match type '#{node.match}'"
102
111
  end
103
112
  when Tokenizer::Regex
104
- node.match = "/#{t.src}/"
113
+ node.match = "/#{token.match.src}/"
105
114
  node.regex_match = true
106
- Regexp.new((node.wildcard ? ".*?" : "^") + t.src)
115
+ Regexp.new((node.wildcard ? ".*?" : "^") + token.match.src)
116
+ when nil
117
+ raise Error, "Empty operation"
107
118
  else
108
- raise Error, "Unexpected token #{t}"
119
+ raise Error, "Unexpected #{token.match}"
109
120
  end
110
121
 
111
122
  # parse the operator (if any)
112
- if (op = tokens[1])
113
- raise Error, "Unexpected #{op}" unless op.is_a? Tokenizer::Char
114
- node.operator = op.char
115
-
116
- arg = tokens[2..-1].reduce("") { |acc, t|
117
- raise Error, "Unexpected #{t}" unless t.is_a? Tokenizer::Char
118
- acc + t.char
119
- }
120
- node.operator_arg = arg == BLANK ? nil : arg
121
-
123
+ if token.operator
124
+ raise Error, "Unexpected #{token.operator} for operator" unless token.operator.is_a? Tokenizer::Char
125
+ node.operator = token.operator.char
126
+ node.operator_arg = token.arg if token.arg and token.arg != BLANK
127
+ node.operator_arg_w_caps = parse_captures! node.operator_arg if node.operator_arg and node.regex_match
122
128
  node.expression =
123
129
  case node.operator
124
130
  when OP_REPLACE
125
- ->(_) { node.operator_arg || BLANK }
131
+ EXP_REPLACE
126
132
  when OP_ADD, OP_SUB, OP_MUL, OP_DIV
127
133
  raise Error, "Operator #{node.operator} is only available for numeric matches" unless node.match == MATCH_NUM
128
134
  raise Error, "Operator #{node.operator} expects an argument" if node.operator_arg.nil?
129
- ->(x) { x.to_i.send(node.operator, node.operator_arg.to_i) }
135
+ EXP_MATH
136
+ when OP_APPEND
137
+ raise Error, "Operator #{node.operator} expects an argument" if node.operator_arg.nil?
138
+ EXP_APPEND
139
+ when OP_PREPEND
140
+ raise Error, "Operator #{node.operator} expects an argument" if node.operator_arg.nil?
141
+ EXP_PREPEND
130
142
  else
131
- raise(Error, "Unknown operator #{node.operator}")
143
+ raise Error, "Unknown operator #{node.operator}"
132
144
  end
133
145
  end
134
146
  end
147
+
148
+ def self.parse_captures!(arg)
149
+ i = 0
150
+ iend = arg.size - 1
151
+ escape = false
152
+ nodes = []
153
+
154
+ until i > iend
155
+ char = arg[i]
156
+ i += 1
157
+
158
+ if escape
159
+ nodes << char
160
+ escape = false
161
+ next
162
+ end
163
+
164
+ case char
165
+ when Tokenizer::ESCAPE
166
+ escape = true
167
+ when VAR
168
+ num = arg[i].to_s
169
+ raise Error, "Capture group number must be between 1 and 9; found '#{num}'" unless num =~ CAP_NUM
170
+ nodes << CaptureGroup.new(num.to_i - 1)
171
+ i += 1
172
+ else
173
+ nodes << char
174
+ end
175
+ end
176
+
177
+ raise Error, "Trailing escape" if escape
178
+ nodes
179
+ end
135
180
  end
136
181
  end
data/lib/fop/tokenizer.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  module Fop
2
2
  class Tokenizer
3
3
  Char = Struct.new(:char)
4
- Op = Struct.new(:tokens)
4
+ Op = Struct.new(:match, :operator, :arg)
5
5
  Regex = Struct.new(:src)
6
6
  Error = Class.new(StandardError)
7
7
 
@@ -22,28 +22,26 @@ module Fop
22
22
  i = 0
23
23
  until i > @end do
24
24
  char = @src[i]
25
+ i += 1
26
+
25
27
  if escape
26
28
  tokens << Char.new(char)
27
29
  escape = false
28
- i += 1
29
30
  next
30
31
  end
31
32
 
32
33
  case char
33
34
  when ESCAPE
34
35
  escape = true
35
- i += 1
36
36
  when OP_OPEN
37
- i, op = operation! i + 1
37
+ i, op = operation! i
38
38
  tokens << op
39
39
  when OP_CLOSE
40
40
  raise "Unexpected #{OP_CLOSE}"
41
41
  when WILDCARD
42
42
  tokens << :wildcard
43
- i += 1
44
43
  else
45
44
  tokens << Char.new(char)
46
- i += 1
47
45
  end
48
46
  end
49
47
 
@@ -54,40 +52,63 @@ module Fop
54
52
  private
55
53
 
56
54
  def operation!(i)
57
- escape = false
58
55
  found_close = false
59
- tokens = []
56
+ op = Op.new(nil, nil, "")
60
57
 
58
+ # Find matcher
59
+ until found_close or op.match or i > @end do
60
+ char = @src[i]
61
+ i += 1
62
+ case char
63
+ when OP_CLOSE
64
+ found_close = true
65
+ when REGEX_MARKER
66
+ i, reg = regex! i
67
+ op.match = reg
68
+ else
69
+ op.match = Char.new(char)
70
+ end
71
+ end
72
+
73
+ # Find operator
74
+ until found_close or op.operator or i > @end do
75
+ char = @src[i]
76
+ i += 1
77
+ case char
78
+ when OP_CLOSE
79
+ found_close = true
80
+ else
81
+ op.operator = Char.new(char)
82
+ end
83
+ end
84
+
85
+ # Find operator arg
86
+ escape = false
61
87
  until found_close or i > @end do
62
88
  char = @src[i]
89
+ i += 1
90
+
63
91
  if escape
64
- tokens << Char.new(char)
92
+ op.arg << char
65
93
  escape = false
66
- i += 1
67
94
  next
68
95
  end
69
96
 
70
97
  case char
71
98
  when ESCAPE
72
99
  escape = true
73
- i += 1
74
100
  when OP_OPEN
75
101
  raise "Unexpected #{OP_OPEN}"
76
102
  when OP_CLOSE
77
103
  found_close = true
78
- i += 1
79
- when REGEX_MARKER
80
- i, reg = regex! i + 1
81
- tokens << reg
82
104
  else
83
- tokens << Char.new(char)
84
- i += 1
105
+ op.arg << char
85
106
  end
86
107
  end
87
108
 
88
109
  raise Error, "Unclosed operation" if !found_close
89
110
  raise Error, "Trailing escape" if escape
90
- return i, Op.new(tokens)
111
+ return i, op
91
112
  end
92
113
 
93
114
  def regex!(i)
data/lib/fop/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Fop
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fop_lang
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Hollinger