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 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: []