fmt 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 +7 -0
- data/README.md +80 -0
- data/lib/fmt/filter.rb +32 -0
- data/lib/fmt/filters.rb +77 -0
- data/lib/fmt/formatter.rb +61 -0
- data/lib/fmt/transformer.rb +38 -0
- data/lib/fmt/version.rb +5 -0
- data/lib/fmt.rb +22 -0
- metadata +178 -0
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
|
data/lib/fmt/filters.rb
ADDED
@@ -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
|
data/lib/fmt/version.rb
ADDED
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: []
|