fmt 0.1.1 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
![CleanShot 2024-07-26 at 01 46 26@2x](https://github.com/user-attachments/assets/bd1d67c6-1182-428b-be05-756f3d330f67)
|
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
|
+
![CleanShot 2024-07-29 at 02 42 19@2x](https://github.com/user-attachments/assets/f67dd215-b848-4a23-bd73-72822cb7d970)
|
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
|
+
![CleanShot 2024-07-29 at 02 45 27@2x](https://github.com/user-attachments/assets/1b933bf4-a62d-4913-b817-d6c69b0e7028)
|
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
|