fmt 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: 546ccf984763e8c2f3590f1fead68a1a7c9f0a874fefd241df21b2cb54550aac
4
+ data.tar.gz: 81cd66780873959882857eeffe05abea6bcf447e4de64946daf089c1efe05ab1
5
+ SHA512:
6
+ metadata.gz: 8c72adc7487d3352361e5ad8f56fda9c623351112dd377542830023e165c98cd6abfac7526b632f4f2a689dddd0bba1c34f901bfdee6ee72d067271e5cb17458
7
+ data.tar.gz: 6c625ceeabf0aec90bda2945247eb648e5bb05ca41cf6f7b7a139c0366d8d8794f239fcb7a3e0e81b24017494e98a4bc826e55423764206986481c85e94e5186
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # Fmt
2
+
3
+ Fmt is a simple template engine based on native Ruby String formatting mechanics.
4
+
5
+ ## Why?
6
+
7
+ I'm currenly using this to help build beautiful CLI applications with Ruby. Plus it's fun.
8
+
9
+ ## Setup
10
+
11
+ ```
12
+ bundle add fmt
13
+ bundle add rainbow # optional
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ Simply create a string with embedded formatting syntax as you'd normally do with `sprintf` or `format`.
19
+ i.e. `"%{...}"`
20
+
21
+ Filters can be chained after the placeholder like so `"%{...}FILTER|FILTER|FILTER"`
22
+ Filters are processed in the order they are specified.
23
+
24
+ You can use native Ruby formatting as well as String methods like `upcase`, `reverse`, `strip`, etc.
25
+ If you have the Rainbow GEM installed, you can also use Rainbow formatting like `red`, `bold`, etc.
26
+
27
+ ### Rendering
28
+
29
+ Basic example:
30
+
31
+ ```ruby
32
+ require "fmt"
33
+
34
+ template = "Hello %{name}cyan|bold"
35
+ result = Fmt(template, name: "World")
36
+ #=> "Hello \e[36m\e[1mWorld\e[0m"
37
+ ```
38
+
39
+ Mix and match native formatting with Rainbow formatting:
40
+
41
+ ```ruby
42
+ require "fmt"
43
+
44
+ template = "Date: %{date}.10s|magenta"
45
+ result = Fmt(template, date: Time.now)
46
+ #=> "Date: \e[35m2024-07-26\e[0m"
47
+ ```
48
+
49
+ Multiline example:
50
+
51
+ ```ruby
52
+ template = <<~T
53
+ Date: %{date}.10s|underline
54
+
55
+ Greetings, %{name}upcase|bold
56
+
57
+ %{message}strip|green
58
+ T
59
+
60
+ result = Fmt(template, date: Time.now, name: "Hopsoft", message: "This is neat!")
61
+ #=> "Date: \e[4m2024-07-26\e[0m\n\nGreetings, \e[1mHOPSOFT\e[0m\n\n\e[32mThis is neat!\e[0m\n"
62
+ ```
63
+
64
+ ### Filters
65
+
66
+ You can also add your own filters to Fmt by calling `Fmt.add_filter(:name, &block)`.
67
+ The block accepts a string and should return a replacement string.
68
+
69
+ ```ruby
70
+ Fmt.add_filter(:repeat20) { |str| str * 20 }
71
+
72
+ template = <<~T
73
+ %{head}repeat20|faint
74
+ %{message}bold
75
+ %{tail}repeat20|faint
76
+ T
77
+
78
+ result = Fmt(template, head: "#", message: "Give it a try!", tail: "#")
79
+ #=> "\e[2m####################\e[0m\n\e[1mGive it a try!\e[0m\n\e[2m####################\e[0m\n"
80
+ ```
data/lib/fmt/filter.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fmt
4
+ class Filter
5
+ def initialize(name, value)
6
+ raise ArgumentError, "value must be a String or Proc" unless value.is_a?(String) || value.is_a?(Proc)
7
+ @name = name
8
+ @value = value
9
+ end
10
+
11
+ attr_reader :name, :value
12
+
13
+ def apply(string)
14
+ case value
15
+ when String then sprintf("%#{filter.value}", string)
16
+ when Proc then filter.value.call(string)
17
+ end
18
+ end
19
+
20
+ def string?
21
+ value.is_a? String
22
+ end
23
+
24
+ def proc?
25
+ value.is_a? Proc
26
+ end
27
+
28
+ def inspect
29
+ "#<#{self.class.name} name=#{name.inspect} value=#{value.inspect}>"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require_relative "filter"
5
+
6
+ module Fmt
7
+ class Filters
8
+ include Enumerable
9
+ include MonitorMixin
10
+
11
+ DELIMITER = "|"
12
+
13
+ NATIVE_FILTERS = %i[
14
+ capitalize
15
+ chomp
16
+ chop
17
+ downcase
18
+ lstrip
19
+ reverse
20
+ rjust
21
+ rstrip
22
+ shellescape
23
+ strip
24
+ succ
25
+ swapcase
26
+ undump
27
+ unicode_normalize
28
+ upcase
29
+ ]
30
+
31
+ def initialize
32
+ super
33
+ @entries = {}
34
+
35
+ NATIVE_FILTERS.each do |name|
36
+ add(name) { |str| str.then(&:"#{name}") }
37
+ end
38
+
39
+ if defined? Rainbow
40
+ Rainbow::Presenter.public_instance_methods(false).each do |name|
41
+ next unless Rainbow::Presenter.instance_method(name).arity == 0
42
+ add(name) { |str| Rainbow(str).public_send(name) }
43
+ end
44
+
45
+ Rainbow::X11ColorNames::NAMES.keys.each do |name|
46
+ add(name) { |str| Rainbow(str).public_send(name) }
47
+ end
48
+ end
49
+ rescue
50
+ # noop
51
+ end
52
+
53
+ def each(&block)
54
+ entries.each(&block)
55
+ end
56
+
57
+ def add(name, filter = nil, &block)
58
+ raise ArgumentError, "filter and block are mutually exclusive" if filter && block
59
+ raise ArgumentError, "filter must be a Proc" unless block || filter.is_a?(Proc)
60
+ entries[name.to_sym] = Filter.new(name, filter || block)
61
+ end
62
+
63
+ alias_method :<<, :add
64
+
65
+ def [](name)
66
+ synchronize { entries[name.to_sym] }
67
+ end
68
+
69
+ def fetch(name, default = nil)
70
+ synchronize { entries.fetch name.to_sym, default }
71
+ end
72
+
73
+ private
74
+
75
+ attr_reader :entries
76
+ end
77
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require_relative "filters"
5
+ require_relative "transformer"
6
+
7
+ module Fmt
8
+ class Formatter
9
+ include Singleton
10
+
11
+ OPEN = /%\{/
12
+ CLOSE = /\}/
13
+ KEY = /\w+(?=\})/
14
+ FILTERS = /[^\s]+(?=\s|$)/
15
+
16
+ attr_reader :filters
17
+
18
+ def format(string, **locals)
19
+ result = string.to_s
20
+ transformer = next_transformer(result)
21
+
22
+ while transformer
23
+ result = transformer.transform(result, **locals)
24
+ transformer = next_transformer(result)
25
+ end
26
+
27
+ result
28
+ end
29
+
30
+ private
31
+
32
+ def initialize
33
+ super
34
+ @filters = Fmt::Filters.new
35
+ end
36
+
37
+ def next_transformer(string)
38
+ scanner = StringScanner.new(string)
39
+
40
+ # 1. advance to the opening delimiter
41
+ scanner.skip_until(OPEN)
42
+
43
+ # 2. extract the key to be transformed
44
+ key = scanner.scan(KEY)
45
+
46
+ # 3. advance to the closing delimiter
47
+ scanner.skip_until(CLOSE) if key
48
+
49
+ # 4. scan for the filters
50
+ filter_string = scanner.scan(FILTERS) if key
51
+
52
+ return nil if key.nil? || filter_string.nil?
53
+
54
+ mapped_filters = filter_string.split(Fmt::Filters::DELIMITER).map do |name|
55
+ filters.fetch name.to_sym, Fmt::Filter.new(name, name)
56
+ end
57
+
58
+ Fmt::Transformer.new(key.to_sym, *mapped_filters, placeholder: "%{#{key}}#{filter_string}".strip)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fmt
4
+ class Transformer
5
+ def initialize(key, *filters, placeholder:)
6
+ @key = key
7
+ @filters = filters
8
+ @placeholder = placeholder
9
+ end
10
+
11
+ attr_reader :key, :filters, :placeholder, :proc_filters, :string_filters
12
+
13
+ def transform(string, **locals)
14
+ return string if filters.none?
15
+
16
+ raise Fmt::Error, "Missing key :#{key} in #{locals.inspect}" unless locals.key?(key)
17
+ replacement = locals[key]
18
+
19
+ filters.each do |filter|
20
+ if filter.string?
21
+ begin
22
+ replacement = format("%#{filter.value}", replacement)
23
+ rescue => error
24
+ raise Fmt::Error, "Invalid filter! #{filter.inspect} Check the spelling and verify that it's registered `Fmt.add_filter(:#{filter.name}, &block)`; #{error.message}"
25
+ end
26
+ elsif filter.proc?
27
+ begin
28
+ replacement = filter.value.call(replacement)
29
+ rescue => error
30
+ raise Fmt::Error, "Error in filter! #{filter.inspect} #{error.message}"
31
+ end
32
+ end
33
+ end
34
+
35
+ string.sub placeholder, replacement
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fmt
4
+ VERSION = "0.1.0"
5
+ end
data/lib/fmt.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "fmt/version"
4
+ require_relative "fmt/formatter"
5
+
6
+ module Fmt
7
+ class Error < StandardError; end
8
+
9
+ class << self
10
+ def formatter
11
+ Formatter.instance
12
+ end
13
+
14
+ def add_filter(...)
15
+ formatter.filters.add(...)
16
+ end
17
+ end
18
+ end
19
+
20
+ def Fmt(...)
21
+ Fmt.formatter.format(...)
22
+ end
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fmt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nate Hopkins (hopsoft)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-07-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: amazing_print
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: magic_frozen_string_literal
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-reporters
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-doc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rainbow
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: yard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: A simple template engine based on native Ruby String formatting mechanics
140
+ email:
141
+ - natehop@gmail.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - README.md
147
+ - lib/fmt.rb
148
+ - lib/fmt/filter.rb
149
+ - lib/fmt/filters.rb
150
+ - lib/fmt/formatter.rb
151
+ - lib/fmt/transformer.rb
152
+ - lib/fmt/version.rb
153
+ homepage: https://github.com/hopsoft/fmt
154
+ licenses:
155
+ - MIT
156
+ metadata:
157
+ homepage_uri: https://github.com/hopsoft/fmt
158
+ source_code_uri: https://github.com/hopsoft/fmt
159
+ post_install_message:
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: 3.0.0
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubygems_version: 3.5.5
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: A simple template engine based on native Ruby String formatting mechanics
178
+ test_files: []