fmt 0.1.1 → 0.1.3
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 +4 -4
- data/README.md +96 -11
- data/lib/fmt/embed.rb +19 -0
- data/lib/fmt/filter_groups/filter_group.rb +56 -0
- data/lib/fmt/filter_groups/rainbow_filter_group.rb +27 -0
- data/lib/fmt/filter_groups/string_filter_group.rb +28 -0
- data/lib/fmt/formatter.rb +24 -25
- data/lib/fmt/scanners/base_scanner.rb +41 -0
- data/lib/fmt/scanners/embed_scanner.rb +56 -0
- data/lib/fmt/scanners/filter_scanner.rb +31 -0
- data/lib/fmt/scanners/key_scanner.rb +15 -0
- data/lib/fmt/scanners.rb +3 -0
- data/lib/fmt/transformer.rb +28 -9
- data/lib/fmt/version.rb +1 -1
- data/lib/fmt.rb +9 -1
- metadata +26 -4
- data/lib/fmt/filters.rb +0 -76
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7aded878eeff57ab27f37b6e76bc34281d94000c95c5d0ae77dde2bee5ab5989
|
4
|
+
data.tar.gz: 0d6390579afbfa629f0e53a127c25b60c04548b831e8ec13b9ebd8ed0880a238
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1235f7508b608760ce2f984cdcb321c01ac2a1cd3d7d5b9d1b586b45614e3b79c38bf900264b569fa469c17e34d2b6e1999984715a9b51c8caa792ee7d080aa9
|
7
|
+
data.tar.gz: '012095a10cbeac34f3457bf18ebe9135513a774984139cd1c7c300cd00d774590eff1826f7af137adbb32acd77f2eb8bffa1d52b48c579422fe43a2e7c3769cf'
|
data/README.md
CHANGED
@@ -1,6 +1,35 @@
|
|
1
|
+
<p align="center">
|
2
|
+
<a href="http://blog.codinghorror.com/the-best-code-is-no-code-at-all/">
|
3
|
+
<img alt="Lines of Code" src="https://img.shields.io/badge/loc-347-47d299.svg" />
|
4
|
+
</a>
|
5
|
+
<a href="https://github.com/testdouble/standard">
|
6
|
+
<img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
|
7
|
+
</a>
|
8
|
+
<a href="https://github.com/sponsors/hopsoft">
|
9
|
+
<img alt="Sponsors" src="https://img.shields.io/github/sponsors/hopsoft?color=eb4aaa&logo=GitHub%20Sponsors" />
|
10
|
+
</a>
|
11
|
+
<a href="https://twitter.com/hopsoft">
|
12
|
+
<img alt="Twitter Follow" src="https://img.shields.io/twitter/url?label=%40hopsoft&style=social&url=https%3A%2F%2Ftwitter.com%2Fhopsoft">
|
13
|
+
</a>
|
14
|
+
</p>
|
15
|
+
|
1
16
|
# Fmt
|
2
17
|
|
3
|
-
|
18
|
+
#### A simple template engine based on native Ruby String formatting mechanics
|
19
|
+
|
20
|
+
<!-- Tocer[start]: Auto-generated, don't remove. -->
|
21
|
+
|
22
|
+
## Table of Contents
|
23
|
+
|
24
|
+
- [Why?](#why)
|
25
|
+
- [Setup](#setup)
|
26
|
+
- [Usage](#usage)
|
27
|
+
- [Formatting](#formatting)
|
28
|
+
- [Filters](#filters)
|
29
|
+
- [Embeds](#embeds)
|
30
|
+
- [Sponsors](#sponsors)
|
31
|
+
|
32
|
+
<!-- Tocer[finish]: Auto-generated, don't remove. -->
|
4
33
|
|
5
34
|
## Why?
|
6
35
|
|
@@ -9,7 +38,7 @@ I'm currenly using this to help build beautiful CLI applications with Ruby. Plus
|
|
9
38
|
## Setup
|
10
39
|
|
11
40
|
```
|
12
|
-
bundle add rainbow # optional
|
41
|
+
bundle add rainbow # <- optional
|
13
42
|
bundle add fmt
|
14
43
|
```
|
15
44
|
|
@@ -35,7 +64,7 @@ Also, you can use Rainbow filters like `bold`, `cyan`, `underline`, et al. if yo
|
|
35
64
|
|
36
65
|
**You can even [register your own filters](#filters).**
|
37
66
|
|
38
|
-
###
|
67
|
+
### Formatting
|
39
68
|
|
40
69
|
Basic example:
|
41
70
|
|
@@ -43,8 +72,11 @@ Basic example:
|
|
43
72
|
require "rainbow"
|
44
73
|
require "fmt"
|
45
74
|
|
75
|
+
Fmt.add_rainbow_filters
|
76
|
+
|
46
77
|
template = "Hello %{name}cyan|bold"
|
47
|
-
|
78
|
+
Fmt template, name: "World"
|
79
|
+
|
48
80
|
#=> "Hello \e[36m\e[1mWorld\e[0m"
|
49
81
|
```
|
50
82
|
|
@@ -56,8 +88,11 @@ Mix and match native formatting with Rainbow formatting:
|
|
56
88
|
require "rainbow"
|
57
89
|
require "fmt"
|
58
90
|
|
91
|
+
Fmt.add_rainbow_filters
|
92
|
+
|
59
93
|
template = "Date: %{date}.10s|magenta"
|
60
|
-
|
94
|
+
Fmt template, date: Time.now
|
95
|
+
|
61
96
|
#=> "Date: \e[35m2024-07-26\e[0m"
|
62
97
|
```
|
63
98
|
|
@@ -69,6 +104,8 @@ Multiline example:
|
|
69
104
|
require "rainbow"
|
70
105
|
require "fmt"
|
71
106
|
|
107
|
+
Fmt.add_rainbow_filters
|
108
|
+
|
72
109
|
template = <<~T
|
73
110
|
Date: %{date}.10s|underline
|
74
111
|
|
@@ -77,7 +114,8 @@ template = <<~T
|
|
77
114
|
%{message}strip|green
|
78
115
|
T
|
79
116
|
|
80
|
-
|
117
|
+
Fmt template, date: Time.now, name: "Hopsoft", message: "This is neat!"
|
118
|
+
|
81
119
|
#=> "Date: \e[4m2024-07-26\e[0m\n\nGreetings, \e[1mHOPSOFT\e[0m\n\n\e[32mThis is neat!\e[0m\n"
|
82
120
|
```
|
83
121
|
|
@@ -92,16 +130,63 @@ The block accepts a string and should return a replacement string.
|
|
92
130
|
require "rainbow"
|
93
131
|
require "fmt"
|
94
132
|
|
95
|
-
Fmt.
|
133
|
+
Fmt.add_rainbow_filters
|
134
|
+
Fmt.add_filter(:ljust) { |val| "".ljust 14, val.to_s }
|
96
135
|
|
97
136
|
template = <<~T
|
98
|
-
%{head}
|
137
|
+
%{head}ljust|faint
|
99
138
|
%{message}bold
|
100
|
-
%{tail}
|
139
|
+
%{tail}ljust|faint
|
101
140
|
T
|
102
141
|
|
103
|
-
|
104
|
-
|
142
|
+
Fmt template, head: "#", message: "Give it a try!", tail: "#"
|
143
|
+
|
144
|
+
#=> "\e[2m##############\e[0m\n\e[1mGive it a try!\e[0m\n\e[2m##############\e[0m\n"
|
105
145
|
```
|
106
146
|
|
107
147
|

|
148
|
+
|
149
|
+
### Embeds
|
150
|
+
|
151
|
+
Templates can be embedded or nested within other templates... as deep as needed!
|
152
|
+
Just wrap the embedded template in double curly braces: `{{EMBEDDED TEMPLATE HERE}}`
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
require "rainbow"
|
156
|
+
require "fmt"
|
157
|
+
|
158
|
+
Fmt.add_rainbow_filters
|
159
|
+
|
160
|
+
template = "%{value}lime {{%{embed_value}red|bold|underline}}"
|
161
|
+
Fmt template, value: "Outer", embed_value: "Inner"
|
162
|
+
|
163
|
+
#=> "\e[38;5;46mOuter\e[0m \e[31m\e[1m\e[4mInner\e[0m"
|
164
|
+
```
|
165
|
+
|
166
|
+

|
167
|
+
|
168
|
+
```ruby
|
169
|
+
template = <<~T
|
170
|
+
|--%{value}yellow|bold|underline
|
171
|
+
| |--{{%{inner_value}green|bold|underline
|
172
|
+
| | |--{{%{deep_value}blue|bold|underline
|
173
|
+
| | | |-- We're in deep!}}}}
|
174
|
+
T
|
175
|
+
|
176
|
+
Fmt template, value: "Outer", inner_value: "Inner", deep_value: "Deep"
|
177
|
+
|
178
|
+
#=> "|--\e[33m\e[1m\e[4mOuter\e[0m\n| |--\e[32m\e[1m\e[4mInner\e[0m\n| | |--\e[34m\e[1m\e[4mDeep\e[0m\n| | | |-- We're in deep!\n"
|
179
|
+
```
|
180
|
+
|
181
|
+

|
182
|
+
|
183
|
+
## Sponsors
|
184
|
+
|
185
|
+
<p align="center">
|
186
|
+
<em>Proudly sponsored by</em>
|
187
|
+
</p>
|
188
|
+
<p align="center">
|
189
|
+
<a href="https://www.clickfunnels.com?utm_source=hopsoft&utm_medium=open-source&utm_campaign=fmt">
|
190
|
+
<img src="https://images.clickfunnel.com/uploads/digital_asset/file/176632/clickfunnels-dark-logo.svg" width="575" />
|
191
|
+
</a>
|
192
|
+
</p>
|
data/lib/fmt/embed.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fmt
|
4
|
+
class Embed
|
5
|
+
def initialize(string)
|
6
|
+
@string = string
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :string
|
10
|
+
|
11
|
+
def placeholder
|
12
|
+
"{{#{string}}}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def format(**locals)
|
16
|
+
Fmt(string, **locals)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "monitor"
|
4
|
+
require_relative "../filter"
|
5
|
+
|
6
|
+
module Fmt
|
7
|
+
class FilterGroup
|
8
|
+
include Enumerable
|
9
|
+
include MonitorMixin
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
super
|
13
|
+
@data = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](name)
|
17
|
+
synchronize { data[name.to_sym] }
|
18
|
+
end
|
19
|
+
|
20
|
+
def add(name, filter_proc = nil, &block)
|
21
|
+
raise ArgumentError, "filter_proc and block are mutually exclusive" if filter_proc && block
|
22
|
+
raise ArgumentError, "filter_proc must be a Proc" unless block || filter_proc.is_a?(Proc)
|
23
|
+
synchronize do
|
24
|
+
data[name.to_sym] = Fmt::Filter.new(name.to_sym, filter_proc || block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def each(&block)
|
29
|
+
synchronize { data.each(&block) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch(name, default = nil)
|
33
|
+
synchronize { data.fetch name.to_sym, default }
|
34
|
+
end
|
35
|
+
|
36
|
+
def key?(name)
|
37
|
+
synchronize { data.key? name.to_sym }
|
38
|
+
end
|
39
|
+
|
40
|
+
alias_method :added?, :key?
|
41
|
+
alias_method :include?, :key?
|
42
|
+
|
43
|
+
def merge!(other)
|
44
|
+
synchronize { data.merge! other.to_h }
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_h
|
49
|
+
data.dup
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
attr_reader :data
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "filter_group"
|
4
|
+
|
5
|
+
module Fmt
|
6
|
+
class RainbowFilterGroup < FilterGroup
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
|
10
|
+
if defined? Rainbow
|
11
|
+
methods = Rainbow::Presenter.public_instance_methods(false).select do |method|
|
12
|
+
Rainbow::Presenter.public_instance_method(method).arity == 0
|
13
|
+
end
|
14
|
+
|
15
|
+
method_names = methods
|
16
|
+
.map { |m| m.name.to_sym }
|
17
|
+
.concat(Rainbow::X11ColorNames::NAMES.keys)
|
18
|
+
|
19
|
+
method_names.each do |name|
|
20
|
+
add(name) { |string| Rainbow(string).public_send name }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
rescue => error
|
24
|
+
puts "Error adding Rainbow filters! #{error.inspect}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "filter_group"
|
4
|
+
|
5
|
+
module Fmt
|
6
|
+
class StringFilterGroup < FilterGroup
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
|
10
|
+
# rubocop:disable Layout/ExtraSpacing AllowForAlignment
|
11
|
+
add :capitalize, ->(s) { s.capitalize }
|
12
|
+
add :chomp, ->(s) { s.chomp }
|
13
|
+
add :chop, ->(s) { s.chop }
|
14
|
+
add :downcase, ->(s) { s.downcase }
|
15
|
+
add :lstrip, ->(s) { s.lstrip }
|
16
|
+
add :reverse, ->(s) { s.reverse }
|
17
|
+
add :rstrip, ->(s) { s.rstrip }
|
18
|
+
add :shellescape, ->(s) { s.shellescape }
|
19
|
+
add :strip, ->(s) { s.strip }
|
20
|
+
add :succ, ->(s) { s.succ }
|
21
|
+
add :swapcase, ->(s) { s.swapcase }
|
22
|
+
add :undump, ->(s) { s.undump }
|
23
|
+
add :unicode_normalize, ->(s) { s.unicode_normalize }
|
24
|
+
add :upcase, ->(s) { s.upcase }
|
25
|
+
# rubocop:enable Layout/ExtraSpacing AllowForAlignment
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/fmt/formatter.rb
CHANGED
@@ -1,20 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "singleton"
|
4
|
-
require_relative "
|
4
|
+
require_relative "filter_groups/filter_group"
|
5
|
+
require_relative "filter_groups/rainbow_filter_group"
|
6
|
+
require_relative "filter_groups/string_filter_group"
|
7
|
+
require_relative "scanners"
|
5
8
|
require_relative "transformer"
|
6
9
|
|
7
10
|
module Fmt
|
8
11
|
class Formatter
|
9
12
|
include Singleton
|
10
13
|
|
11
|
-
OPEN = /%\{/
|
12
|
-
CLOSE = /\}/
|
13
|
-
KEY = /\w+(?=\})/
|
14
|
-
FILTERS = /[^\s]+(?=\s|$)/
|
15
|
-
|
16
14
|
attr_reader :filters
|
17
15
|
|
16
|
+
def add_rainbow_filters
|
17
|
+
filters.merge! Fmt::RainbowFilterGroup.new.to_h
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_filter(...)
|
21
|
+
filters.add(...)
|
22
|
+
end
|
23
|
+
|
18
24
|
def format(string, **locals)
|
19
25
|
result = string.to_s
|
20
26
|
transformer = next_transformer(result)
|
@@ -31,31 +37,24 @@ module Fmt
|
|
31
37
|
|
32
38
|
def initialize
|
33
39
|
super
|
34
|
-
@filters = Fmt::
|
40
|
+
@filters = Fmt::FilterGroup.new.merge!(Fmt::StringFilterGroup.new)
|
35
41
|
end
|
36
42
|
|
37
43
|
def next_transformer(string)
|
38
|
-
|
44
|
+
embed_scanner = Fmt::EmbedScanner.new(string)
|
45
|
+
embed_scanner.scan
|
39
46
|
|
40
|
-
|
41
|
-
|
47
|
+
key_scanner = Fmt::KeyScanner.new(string)
|
48
|
+
key = key_scanner.scan
|
49
|
+
return nil unless key
|
42
50
|
|
43
|
-
|
44
|
-
|
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
|
51
|
+
filter_scanner = Fmt::FilterScanner.new(key_scanner.rest, registered_filters: filters)
|
52
|
+
filter_string = filter_scanner.scan
|
57
53
|
|
58
|
-
Fmt::Transformer.new
|
54
|
+
Fmt::Transformer.new key.to_sym,
|
55
|
+
embeds: embed_scanner.embeds,
|
56
|
+
filters: filter_scanner.filters,
|
57
|
+
placeholder: "%{#{key}}#{filter_string}".strip
|
59
58
|
end
|
60
59
|
end
|
61
60
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require "strscan"
|
5
|
+
|
6
|
+
module Fmt
|
7
|
+
class BaseScanner
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def initialize(string)
|
11
|
+
@string_scanner = StringScanner.new(string)
|
12
|
+
end
|
13
|
+
|
14
|
+
def_delegators :string_scanner, :string, :rest
|
15
|
+
attr_reader :value
|
16
|
+
|
17
|
+
def performed?
|
18
|
+
!!@performed
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset
|
22
|
+
@performed = false
|
23
|
+
string_scanner.reset
|
24
|
+
end
|
25
|
+
|
26
|
+
def scan
|
27
|
+
return if performed?
|
28
|
+
@performed = true
|
29
|
+
perform
|
30
|
+
value
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
attr_reader :string_scanner
|
36
|
+
|
37
|
+
def perform
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_scanner"
|
4
|
+
require_relative "../embed"
|
5
|
+
|
6
|
+
module Fmt
|
7
|
+
class EmbedScanner < BaseScanner
|
8
|
+
def initialize(string, root: nil)
|
9
|
+
super(string)
|
10
|
+
@root ||= root || self
|
11
|
+
@embeds = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def embeds
|
15
|
+
root? ? @embeds : root.embeds
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :root
|
19
|
+
|
20
|
+
def root?
|
21
|
+
root == self
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def includes_embed?(string)
|
27
|
+
string.match?(/[{]{2}/)
|
28
|
+
end
|
29
|
+
|
30
|
+
def next_scanner(string)
|
31
|
+
Fmt::EmbedScanner.new string, root: root
|
32
|
+
end
|
33
|
+
|
34
|
+
def scan_embeds(string)
|
35
|
+
return unless includes_embed?(string)
|
36
|
+
|
37
|
+
string = string[string.index(/[{]{2}/) + 2..]
|
38
|
+
scanner = next_scanner(string)
|
39
|
+
while (embed = scanner.scan)
|
40
|
+
embeds << Fmt::Embed.new(embed)
|
41
|
+
scanner = next_scanner(scanner.rest)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def perform?
|
46
|
+
!string.start_with?("}")
|
47
|
+
end
|
48
|
+
|
49
|
+
def perform
|
50
|
+
return unless perform?
|
51
|
+
scan_embeds string # <------------------------------------ extract embeds
|
52
|
+
string_scanner.scan_until(/[{]{2}/) # <------------------- advance to start
|
53
|
+
@value = string_scanner.scan_until(/[^}]*(?=[}]{2})/) # <- extract value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_scanner"
|
4
|
+
|
5
|
+
module Fmt
|
6
|
+
class FilterScanner < BaseScanner
|
7
|
+
DELIMITER = "|"
|
8
|
+
|
9
|
+
def initialize(string, registered_filters:)
|
10
|
+
@registered_filters = registered_filters
|
11
|
+
@filters = []
|
12
|
+
super(string)
|
13
|
+
end
|
14
|
+
|
15
|
+
alias_method :filter_string, :value
|
16
|
+
attr_reader :filters
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
attr_reader :registered_filters
|
21
|
+
|
22
|
+
def perform
|
23
|
+
@value = string_scanner.scan(/[^\s%]+/) # <- extract value
|
24
|
+
return unless string_scanner.matched?
|
25
|
+
|
26
|
+
@filters = value.split(DELIMITER)&.map do |name|
|
27
|
+
registered_filters.fetch name.to_sym, Fmt::Filter.new(name, name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_scanner"
|
4
|
+
|
5
|
+
module Fmt
|
6
|
+
class KeyScanner < BaseScanner
|
7
|
+
protected
|
8
|
+
|
9
|
+
def perform
|
10
|
+
string_scanner.skip_until(/%[{]/) # <------------------------------ advance to start
|
11
|
+
@value = string_scanner.scan(/\w+/) if string_scanner.matched? # <- extract value
|
12
|
+
string_scanner.scan(/[}]/) if string_scanner.matched? # <---------- advance to end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/fmt/scanners.rb
ADDED
data/lib/fmt/transformer.rb
CHANGED
@@ -2,37 +2,56 @@
|
|
2
2
|
|
3
3
|
module Fmt
|
4
4
|
class Transformer
|
5
|
-
def initialize(key,
|
5
|
+
def initialize(key, embeds:, filters:, placeholder:)
|
6
6
|
@key = key
|
7
|
+
@embeds = embeds
|
7
8
|
@filters = filters
|
8
9
|
@placeholder = placeholder
|
9
10
|
end
|
10
11
|
|
11
|
-
attr_reader :key, :filters, :placeholder, :proc_filters, :string_filters
|
12
|
+
attr_reader :key, :embeds, :filters, :placeholder, :proc_filters, :string_filters
|
12
13
|
|
13
14
|
def transform(string, **locals)
|
14
|
-
|
15
|
+
string = transform_embeds(string, **locals)
|
16
|
+
|
17
|
+
raise Fmt::Error, "Missing key! :#{key} <string=#{string.inspect} locals=#{locals.inspect}>" unless locals.key?(key)
|
15
18
|
|
16
|
-
raise Fmt::Error, "Missing key :#{key} in #{locals.inspect}" unless locals.key?(key)
|
17
19
|
replacement = locals[key]
|
18
20
|
|
19
21
|
filters.each do |filter|
|
20
22
|
if filter.string?
|
21
23
|
begin
|
22
|
-
replacement =
|
23
|
-
rescue
|
24
|
-
raise Fmt::Error,
|
24
|
+
replacement = sprintf("%#{filter.value}", replacement)
|
25
|
+
rescue
|
26
|
+
raise Fmt::Error, <<~MSG
|
27
|
+
Invalid filter! #{filter.inspect}
|
28
|
+
Verify it has been properly registered. SEE: Fmt.add_filter(:#{filter.name}, &block)
|
29
|
+
MSG
|
25
30
|
end
|
26
31
|
elsif filter.proc?
|
27
32
|
begin
|
28
33
|
replacement = filter.value.call(replacement)
|
29
34
|
rescue => error
|
30
|
-
raise Fmt::Error,
|
35
|
+
raise Fmt::Error, <<~MSG
|
36
|
+
Error in filter! #{filter.inspect}
|
37
|
+
#{error.message}
|
38
|
+
MSG
|
31
39
|
end
|
32
40
|
end
|
33
41
|
end
|
34
42
|
|
35
|
-
string.sub
|
43
|
+
result = string.sub(placeholder, replacement)
|
44
|
+
defined?(Rainbow) ? Rainbow(result) : result
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def transform_embeds(string, **locals)
|
50
|
+
while embeds.any?
|
51
|
+
embed = embeds.shift
|
52
|
+
string = string.sub(embed.placeholder, embed.format(**locals))
|
53
|
+
end
|
54
|
+
string
|
36
55
|
end
|
37
56
|
end
|
38
57
|
end
|
data/lib/fmt/version.rb
CHANGED
data/lib/fmt.rb
CHANGED
@@ -11,8 +11,16 @@ module Fmt
|
|
11
11
|
Formatter.instance
|
12
12
|
end
|
13
13
|
|
14
|
+
def filters
|
15
|
+
formatter.filters
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_rainbow_filters
|
19
|
+
formatter.add_rainbow_filters
|
20
|
+
end
|
21
|
+
|
14
22
|
def add_filter(...)
|
15
|
-
formatter.
|
23
|
+
formatter.add_filter(...)
|
16
24
|
end
|
17
25
|
end
|
18
26
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fmt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Hopkins (hopsoft)
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-07-
|
11
|
+
date: 2024-07-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: amazing_print
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: tocer
|
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'
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
140
|
name: yard
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -145,9 +159,17 @@ extra_rdoc_files: []
|
|
145
159
|
files:
|
146
160
|
- README.md
|
147
161
|
- lib/fmt.rb
|
162
|
+
- lib/fmt/embed.rb
|
148
163
|
- lib/fmt/filter.rb
|
149
|
-
- lib/fmt/
|
164
|
+
- lib/fmt/filter_groups/filter_group.rb
|
165
|
+
- lib/fmt/filter_groups/rainbow_filter_group.rb
|
166
|
+
- lib/fmt/filter_groups/string_filter_group.rb
|
150
167
|
- lib/fmt/formatter.rb
|
168
|
+
- lib/fmt/scanners.rb
|
169
|
+
- lib/fmt/scanners/base_scanner.rb
|
170
|
+
- lib/fmt/scanners/embed_scanner.rb
|
171
|
+
- lib/fmt/scanners/filter_scanner.rb
|
172
|
+
- lib/fmt/scanners/key_scanner.rb
|
151
173
|
- lib/fmt/transformer.rb
|
152
174
|
- lib/fmt/version.rb
|
153
175
|
homepage: https://github.com/hopsoft/fmt
|
@@ -171,7 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
171
193
|
- !ruby/object:Gem::Version
|
172
194
|
version: '0'
|
173
195
|
requirements: []
|
174
|
-
rubygems_version: 3.5.
|
196
|
+
rubygems_version: 3.5.11
|
175
197
|
signing_key:
|
176
198
|
specification_version: 4
|
177
199
|
summary: A simple template engine based on native Ruby String formatting mechanics
|
data/lib/fmt/filters.rb
DELETED
@@ -1,76 +0,0 @@
|
|
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
|
-
rstrip
|
21
|
-
shellescape
|
22
|
-
strip
|
23
|
-
succ
|
24
|
-
swapcase
|
25
|
-
undump
|
26
|
-
unicode_normalize
|
27
|
-
upcase
|
28
|
-
]
|
29
|
-
|
30
|
-
def initialize
|
31
|
-
super
|
32
|
-
@entries = {}
|
33
|
-
|
34
|
-
NATIVE_FILTERS.each do |name|
|
35
|
-
add(name) { |str| str.then(&:"#{name}") }
|
36
|
-
end
|
37
|
-
|
38
|
-
if defined? Rainbow
|
39
|
-
Rainbow::Presenter.public_instance_methods(false).each do |name|
|
40
|
-
next unless Rainbow::Presenter.public_instance_method(name).arity == 0
|
41
|
-
add(name) { |str| Rainbow(str).public_send(name) }
|
42
|
-
end
|
43
|
-
|
44
|
-
Rainbow::X11ColorNames::NAMES.keys.each do |name|
|
45
|
-
add(name) { |str| Rainbow(str).public_send(name) }
|
46
|
-
end
|
47
|
-
end
|
48
|
-
rescue
|
49
|
-
# noop
|
50
|
-
end
|
51
|
-
|
52
|
-
def each(&block)
|
53
|
-
entries.each(&block)
|
54
|
-
end
|
55
|
-
|
56
|
-
def add(name, filter = nil, &block)
|
57
|
-
raise ArgumentError, "filter and block are mutually exclusive" if filter && block
|
58
|
-
raise ArgumentError, "filter must be a Proc" unless block || filter.is_a?(Proc)
|
59
|
-
entries[name.to_sym] = Filter.new(name, filter || block)
|
60
|
-
end
|
61
|
-
|
62
|
-
alias_method :<<, :add
|
63
|
-
|
64
|
-
def [](name)
|
65
|
-
synchronize { entries[name.to_sym] }
|
66
|
-
end
|
67
|
-
|
68
|
-
def fetch(name, default = nil)
|
69
|
-
synchronize { entries.fetch name.to_sym, default }
|
70
|
-
end
|
71
|
-
|
72
|
-
private
|
73
|
-
|
74
|
-
attr_reader :entries
|
75
|
-
end
|
76
|
-
end
|