fop_lang 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Fop
2
+ VERSION = "0.1.0"
3
+ end
data/lib/fop_lang.rb ADDED
@@ -0,0 +1,12 @@
1
+ require_relative 'fop/version'
2
+ require_relative 'fop/program'
3
+
4
+ def Fop(src)
5
+ ::Fop::Program.new(src)
6
+ end
7
+
8
+ module Fop
9
+ def self.compile(src)
10
+ Program.new(src)
11
+ end
12
+ end
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: []