fop_lang 0.1.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 +7 -0
- data/README.md +44 -0
- data/lib/fop/nodes.rb +69 -0
- data/lib/fop/parser.rb +93 -0
- data/lib/fop/program.rb +24 -0
- data/lib/fop/tokenizer.rb +34 -0
- data/lib/fop/version.rb +3 -0
- data/lib/fop_lang.rb +12 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 62ded9f974ca670a6eca5d4b293f98195af25ab476e9b3571e7def5299499fa9
|
4
|
+
data.tar.gz: b53b6e276ee31f122571a096dd534ed5c1407d27384fab78d216886322751fcb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 45509b09122e4f76f1f8219d8c0f094bc5ac77231dec25f6e0e9818932b68186d74c71dd74db80638dcfc9bae210892b563e660004cfd32c25f439c761c3edc2
|
7
|
+
data.tar.gz: 49c6153cf728e909e60bc8fa1f1214dcdd8f9532703c8bc3455999aac1aa421c629370e1c7669bc9a846b5886fa2f10b7f5b698426cae2830e73ca9623a32a4a
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# fop_lang
|
2
|
+
|
3
|
+
Fop is a tiny expression language implemented in Ruby for text filtering and modification.
|
4
|
+
|
5
|
+
## Examples
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
f = Fop("release-{N}.{N+1}.{N=0}")
|
9
|
+
|
10
|
+
puts f.apply("release-5.99.1")
|
11
|
+
=> "release-5.100.0"
|
12
|
+
|
13
|
+
puts f.apply("release-5")
|
14
|
+
=> nil
|
15
|
+
# doesn't match the pattern
|
16
|
+
```
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
f = Fop("release-{N=5}.{N+1}.{N=0}")
|
20
|
+
|
21
|
+
puts f.apply("release-4.99.1")
|
22
|
+
=> "release-5.100.0"
|
23
|
+
```
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
f = Fop("release-*{N=5}.{N+100}.{N=0}")
|
27
|
+
|
28
|
+
puts f.apply("release-foo-4.100.1")
|
29
|
+
=> "release-foo-5.200.0"
|
30
|
+
```
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
f = Fop("release-{N=5}.{N+1}.{N=0}{*=}")
|
34
|
+
|
35
|
+
puts f.apply("release-4.100.1.foo.bar")
|
36
|
+
=> "release-5.101.0"
|
37
|
+
```
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
f = Fop("{W=version}-{N=5}.{N+1}.{N=0}")
|
41
|
+
|
42
|
+
puts f.apply("release-4.100.1")
|
43
|
+
=> "version-5.101.0"
|
44
|
+
```
|
data/lib/fop/nodes.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module Fop
|
2
|
+
module Nodes
|
3
|
+
Text = Struct.new(:wildcard, :str) do
|
4
|
+
def consume!(input)
|
5
|
+
@regex ||= Regexp.new((wildcard ? ".*" : "^") + Regexp.escape(str))
|
6
|
+
input.slice!(@regex)
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
w = wildcard ? "*" : nil
|
11
|
+
"Text #{w}#{str}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Match = Struct.new(:wildcard, :tokens) do
|
16
|
+
NUM = "N".freeze
|
17
|
+
WORD = "W".freeze
|
18
|
+
WILD = "*".freeze
|
19
|
+
BLANK = "".freeze
|
20
|
+
|
21
|
+
def consume!(input)
|
22
|
+
if (val = input.slice!(@regex))
|
23
|
+
@expression && val != BLANK ? @expression.call(val) : val
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
w = wildcard ? "*" : nil
|
29
|
+
@op ? "#{w}#{@match} #{@op} #{@arg}" : "#{w}#{@match}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse!
|
33
|
+
match = tokens.shift || raise(ParserError, "Empty match")
|
34
|
+
raise ParserError, "Unexpected #{match}" unless match.is_a? Tokenizer::Char
|
35
|
+
|
36
|
+
@match = match.char
|
37
|
+
@regex =
|
38
|
+
case @match
|
39
|
+
when NUM then Regexp.new((wildcard ? ".*?" : "^") + "[0-9]+")
|
40
|
+
when WORD then Regexp.new((wildcard ? ".*?" : "^") + "[a-zA-Z]+")
|
41
|
+
when WILD then /.*/
|
42
|
+
else raise ParserError, "Unknown match type '#{@match}'"
|
43
|
+
end
|
44
|
+
|
45
|
+
if (op = tokens.shift)
|
46
|
+
raise ParserError, "Unexpected #{op}" unless op.is_a? Tokenizer::Char
|
47
|
+
arg = tokens.reduce("") { |acc, t|
|
48
|
+
raise ParserError, "Unexpected #{t}" unless t.is_a? Tokenizer::Char
|
49
|
+
acc + t.char
|
50
|
+
}
|
51
|
+
|
52
|
+
@op = op.char
|
53
|
+
@arg = arg == BLANK ? nil : arg
|
54
|
+
@expression =
|
55
|
+
case @op
|
56
|
+
when "=" then ->(_) { @arg || BLANK }
|
57
|
+
when "+", "-", "*", "/"
|
58
|
+
raise ParserError, "Operator #{@op} is only available for numeric matches" unless @match == NUM
|
59
|
+
raise ParserError, "Operator #{@op} expects an argument" if @arg.nil?
|
60
|
+
->(x) { x.to_i.send(@op, @arg.to_i) }
|
61
|
+
else raise ParserError, "Unknown operator #{@op}"
|
62
|
+
end
|
63
|
+
else
|
64
|
+
@op, @arg, @expression = nil, nil, nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/fop/parser.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require_relative 'nodes'
|
2
|
+
|
3
|
+
module Fop
|
4
|
+
module Parser
|
5
|
+
Error = Class.new(StandardError)
|
6
|
+
|
7
|
+
def self.parse!(tokens)
|
8
|
+
stack = []
|
9
|
+
current_el = nil
|
10
|
+
|
11
|
+
tokens.each { |token|
|
12
|
+
case current_el
|
13
|
+
when nil
|
14
|
+
current_el = new_element token
|
15
|
+
when :wildcard
|
16
|
+
current_el = new_element token, true
|
17
|
+
raise Error, "Unexpected * after wildcard" if current_el == :wildcard
|
18
|
+
when Nodes::Text
|
19
|
+
current_el = parse_text stack, current_el, token
|
20
|
+
when Nodes::Match
|
21
|
+
current_el = parse_match stack, current_el, token
|
22
|
+
else
|
23
|
+
raise Error, "Unexpected token #{token} in #{current_el}"
|
24
|
+
end
|
25
|
+
}
|
26
|
+
|
27
|
+
case current_el
|
28
|
+
when nil
|
29
|
+
# noop
|
30
|
+
when :wildcard
|
31
|
+
stack << Nodes::Text.new(true, "")
|
32
|
+
when Nodes::Text
|
33
|
+
stack << current_el
|
34
|
+
when Nodes::Match
|
35
|
+
raise Error, "Unclosed match"
|
36
|
+
end
|
37
|
+
|
38
|
+
stack
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def self.new_element(token, wildcard = false)
|
44
|
+
case token
|
45
|
+
when Tokenizer::Char
|
46
|
+
Nodes::Text.new(wildcard, token.char.clone)
|
47
|
+
when :match_open
|
48
|
+
Nodes::Match.new(wildcard, [])
|
49
|
+
when :match_close
|
50
|
+
raise ParserError, "Unmatched }"
|
51
|
+
when :wildcard
|
52
|
+
:wildcard
|
53
|
+
else
|
54
|
+
raise ParserError, "Unexpected #{token}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.parse_text(stack, text_el, token)
|
59
|
+
case token
|
60
|
+
when :match_open
|
61
|
+
stack << text_el
|
62
|
+
Nodes::Match.new(false, [])
|
63
|
+
when :match_close
|
64
|
+
raise ParserError.new, "Unexpected }"
|
65
|
+
when Tokenizer::Char
|
66
|
+
text_el.str << token.char
|
67
|
+
text_el
|
68
|
+
when :wildcard
|
69
|
+
stack << text_el
|
70
|
+
:wildcard
|
71
|
+
else
|
72
|
+
raise ParserError, "Unexpected #{token}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.parse_match(stack, match_el, token)
|
77
|
+
case token
|
78
|
+
when Tokenizer::Char
|
79
|
+
match_el.tokens << token
|
80
|
+
match_el
|
81
|
+
when :wildcard
|
82
|
+
match_el.tokens << Tokenizer::Char.new("*").freeze
|
83
|
+
match_el
|
84
|
+
when :match_close
|
85
|
+
match_el.parse!
|
86
|
+
stack << match_el
|
87
|
+
nil
|
88
|
+
else
|
89
|
+
raise ParserError, "Unexpected #{token}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/fop/program.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'tokenizer'
|
2
|
+
require_relative 'parser'
|
3
|
+
|
4
|
+
module Fop
|
5
|
+
class Program
|
6
|
+
attr_reader :nodes
|
7
|
+
|
8
|
+
def initialize(src)
|
9
|
+
tokens = Tokenizer.tokenize! src
|
10
|
+
@nodes = Parser.parse! tokens
|
11
|
+
end
|
12
|
+
|
13
|
+
def apply(input)
|
14
|
+
input = input.clone
|
15
|
+
output =
|
16
|
+
@nodes.reduce("") { |acc, token|
|
17
|
+
section = token.consume!(input)
|
18
|
+
return nil if section.nil?
|
19
|
+
acc + section.to_s
|
20
|
+
}
|
21
|
+
input.empty? ? output : nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Fop
|
2
|
+
module Tokenizer
|
3
|
+
Char = Struct.new(:char)
|
4
|
+
Error = Class.new(StandardError)
|
5
|
+
|
6
|
+
def self.tokenize!(src)
|
7
|
+
tokens = []
|
8
|
+
escape = false
|
9
|
+
src.each_char { |char|
|
10
|
+
if escape
|
11
|
+
tokens << Char.new(char)
|
12
|
+
escape = false
|
13
|
+
next
|
14
|
+
end
|
15
|
+
|
16
|
+
case char
|
17
|
+
when "\\".freeze
|
18
|
+
escape = true
|
19
|
+
when "{".freeze
|
20
|
+
tokens << :match_open
|
21
|
+
when "}".freeze
|
22
|
+
tokens << :match_close
|
23
|
+
when "*".freeze
|
24
|
+
tokens << :wildcard
|
25
|
+
else
|
26
|
+
tokens << Char.new(char)
|
27
|
+
end
|
28
|
+
}
|
29
|
+
|
30
|
+
raise Error, "Trailing escape" if escape
|
31
|
+
tokens
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/fop/version.rb
ADDED
data/lib/fop_lang.rb
ADDED
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fop_lang
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jordan Hollinger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-08-15 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A micro expression language for Filter and OPerations on text
|
14
|
+
email: jordan.hollinger@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- README.md
|
20
|
+
- lib/fop/nodes.rb
|
21
|
+
- lib/fop/parser.rb
|
22
|
+
- lib/fop/program.rb
|
23
|
+
- lib/fop/tokenizer.rb
|
24
|
+
- lib/fop/version.rb
|
25
|
+
- lib/fop_lang.rb
|
26
|
+
homepage: https://jhollinger.github.io/fop-lang-rb/
|
27
|
+
licenses:
|
28
|
+
- MIT
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 2.3.0
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubygems_version: 3.0.3
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: A micro expression language
|
49
|
+
test_files: []
|