fmt 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55fc16022e0fcb4b8815d6e7242106018e5786a9d610f38f8040cbb31cadb1a0
4
- data.tar.gz: 185f056e8450fb1facfe15c7c637e9b49d2a19a2270689b8e12953c664393e10
3
+ metadata.gz: a4bafcbfda352b24f4203057c8ae8c8df4ddc109727fd610250d54c1db02ea2a
4
+ data.tar.gz: 1e672363e323f01757e1590441d8f836bba9b03936b4551189cd040a942936f2
5
5
  SHA512:
6
- metadata.gz: 707ed8686b167ecba644d7a2358cf179670656cd9a7eb77f6d9408b7b8d5f07a9c204ca94fcd928e8630ca5633ce716c0fb600c4bca8d8e6992a1f682fcd76dc
7
- data.tar.gz: 4b350d2ce4dd9ceeca3b7640d392b806c9698607bae33db99f18a39248898dbeef10100040f2d78dfc66f95d78839a982cf9a1cb3391129c70347f68f043bf4d
6
+ metadata.gz: 39a4f811f2a9ee86e82fd0e7ce557aa6f22971cd31bde8c5a717b47f74b35119c8d7616580dac5de18851329edd304a72549ecd8239c2104caea3f41d4a3da7e
7
+ data.tar.gz: 75fe78f4908b6b94eb7fe37ba0d76819562d7005405d72d3c14ccb93b55d465da95e890becd9794b53eb7c3d44970a6f14ba3d62d45c1eb78e1cf31810e4885f
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-316-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
- Fmt is a simple template engine based on native Ruby String formatting mechanics.
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
 
@@ -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
- ### Rendering
67
+ ### Formatting
39
68
 
40
69
  Basic example:
41
70
 
@@ -44,7 +73,8 @@ require "rainbow"
44
73
  require "fmt"
45
74
 
46
75
  template = "Hello %{name}cyan|bold"
47
- result = Fmt(template, name: "World")
76
+ Fmt template, name: "World"
77
+
48
78
  #=> "Hello \e[36m\e[1mWorld\e[0m"
49
79
  ```
50
80
 
@@ -57,7 +87,8 @@ require "rainbow"
57
87
  require "fmt"
58
88
 
59
89
  template = "Date: %{date}.10s|magenta"
60
- result = Fmt(template, date: Time.now)
90
+ Fmt template, date: Time.now
91
+
61
92
  #=> "Date: \e[35m2024-07-26\e[0m"
62
93
  ```
63
94
 
@@ -77,7 +108,8 @@ template = <<~T
77
108
  %{message}strip|green
78
109
  T
79
110
 
80
- result = Fmt(template, date: Time.now, name: "Hopsoft", message: "This is neat!")
111
+ Fmt template, date: Time.now, name: "Hopsoft", message: "This is neat!"
112
+
81
113
  #=> "Date: \e[4m2024-07-26\e[0m\n\nGreetings, \e[1mHOPSOFT\e[0m\n\n\e[32mThis is neat!\e[0m\n"
82
114
  ```
83
115
 
@@ -92,16 +124,60 @@ The block accepts a string and should return a replacement string.
92
124
  require "rainbow"
93
125
  require "fmt"
94
126
 
95
- Fmt.add_filter(:repeat20) { |str| str * 20 }
127
+ Fmt.add_filter(:ljust) { |val| "".ljust 14, val.to_s }
96
128
 
97
129
  template = <<~T
98
- %{head}repeat20|faint
130
+ %{head}ljust|faint
99
131
  %{message}bold
100
- %{tail}repeat20|faint
132
+ %{tail}ljust|faint
101
133
  T
102
134
 
103
- result = Fmt(template, head: "#", message: "Give it a try!", tail: "#")
104
- #=> "\e[2m####################\e[0m\n\e[1mGive it a try!\e[0m\n\e[2m####################\e[0m\n"
135
+ Fmt template, head: "#", message: "Give it a try!", tail: "#"
136
+
137
+ #=> "\e[2m##############\e[0m\n\e[1mGive it a try!\e[0m\n\e[2m##############\e[0m\n"
105
138
  ```
106
139
 
107
140
  ![CleanShot 2024-07-26 at 01 46 26@2x](https://github.com/user-attachments/assets/bd1d67c6-1182-428b-be05-756f3d330f67)
141
+
142
+ ### Embeds
143
+
144
+ Templates can be embedded or nested within other templates... as deep as needed!
145
+ Just wrap the embedded template in double curly braces: `{{EMBEDDED TEMPLATE HERE}}`
146
+
147
+ ```ruby
148
+ require "rainbow"
149
+ require "fmt"
150
+
151
+ template = "%{value}lime {{%{embed_value}red|bold|underline}}"
152
+ Fmt template, value: "Outer", embed_value: "Inner"
153
+
154
+ #=> "\e[38;5;46mOuter\e[0m \e[31m\e[1m\e[4mInner\e[0m"
155
+ ```
156
+
157
+ ![CleanShot 2024-07-29 at 02 42 19@2x](https://github.com/user-attachments/assets/f67dd215-b848-4a23-bd73-72822cb7d970)
158
+
159
+ ```ruby
160
+ template = <<~T
161
+ |--%{value}yellow|bold|underline
162
+ | |--{{%{inner_value}green|bold|underline
163
+ | | |--{{%{deep_value}blue|bold|underline
164
+ | | | |-- We're in deep!}}}}
165
+ T
166
+
167
+ Fmt template, value: "Outer", inner_value: "Inner", deep_value: "Deep"
168
+
169
+ #=> "|--\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"
170
+ ```
171
+
172
+ ![CleanShot 2024-07-29 at 02 45 27@2x](https://github.com/user-attachments/assets/1b933bf4-a62d-4913-b817-d6c69b0e7028)
173
+
174
+ ## Sponsors
175
+
176
+ <p align="center">
177
+ <em>Proudly sponsored by</em>
178
+ </p>
179
+ <p align="center">
180
+ <a href="https://www.clickfunnels.com?utm_source=hopsoft&utm_medium=open-source&utm_campaign=fmt">
181
+ <img src="https://images.clickfunnel.com/uploads/digital_asset/file/176632/clickfunnels-dark-logo.svg" width="575" />
182
+ </a>
183
+ </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
data/lib/fmt/filters.rb CHANGED
@@ -8,8 +8,6 @@ module Fmt
8
8
  include Enumerable
9
9
  include MonitorMixin
10
10
 
11
- DELIMITER = "|"
12
-
13
11
  NATIVE_FILTERS = %i[
14
12
  capitalize
15
13
  chomp
@@ -36,17 +34,19 @@ module Fmt
36
34
  end
37
35
 
38
36
  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
37
+ begin
38
+ Rainbow::Presenter.public_instance_methods(false).each do |name|
39
+ next unless Rainbow::Presenter.public_instance_method(name).arity == 0
40
+ add(name) { |str| Rainbow(str).public_send(name) }
41
+ end
43
42
 
44
- Rainbow::X11ColorNames::NAMES.keys.each do |name|
45
- add(name) { |str| Rainbow(str).public_send(name) }
43
+ Rainbow::X11ColorNames::NAMES.keys.each do |name|
44
+ add(name) { |str| Rainbow(str).public_send(name) }
45
+ end
46
+ rescue => error
47
+ puts "Error adding Rainbow filters! #{error.inspect}"
46
48
  end
47
49
  end
48
- rescue
49
- # noop
50
50
  end
51
51
 
52
52
  def each(&block)
data/lib/fmt/formatter.rb CHANGED
@@ -2,17 +2,13 @@
2
2
 
3
3
  require "singleton"
4
4
  require_relative "filters"
5
+ require_relative "scanners"
5
6
  require_relative "transformer"
6
7
 
7
8
  module Fmt
8
9
  class Formatter
9
10
  include Singleton
10
11
 
11
- OPEN = /%\{/
12
- CLOSE = /\}/
13
- KEY = /\w+(?=\})/
14
- FILTERS = /[^\s]+(?=\s|$)/
15
-
16
12
  attr_reader :filters
17
13
 
18
14
  def format(string, **locals)
@@ -35,27 +31,20 @@ module Fmt
35
31
  end
36
32
 
37
33
  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)
34
+ embed_scanner = Fmt::EmbedScanner.new(string)
35
+ embed_scanner.scan
45
36
 
46
- # 3. advance to the closing delimiter
47
- scanner.skip_until(CLOSE) if key
37
+ key_scanner = Fmt::KeyScanner.new(string)
38
+ key = key_scanner.scan
39
+ return nil unless key
48
40
 
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
41
+ filter_scanner = Fmt::FilterScanner.new(key_scanner.rest, registered_filters: filters)
42
+ filter_string = filter_scanner.scan
57
43
 
58
- Fmt::Transformer.new(key.to_sym, *mapped_filters, placeholder: "%{#{key}}#{filter_string}".strip)
44
+ Fmt::Transformer.new key.to_sym,
45
+ embeds: embed_scanner.embeds,
46
+ filters: filter_scanner.filters,
47
+ placeholder: "%{#{key}}#{filter_string}".strip
59
48
  end
60
49
  end
61
50
  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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.join(__dir__, "scanners", "*.rb")].each { |file| require file }
@@ -2,37 +2,62 @@
2
2
 
3
3
  module Fmt
4
4
  class Transformer
5
- def initialize(key, *filters, placeholder:)
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
- return string if filters.none?
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 = format("%#{filter.value}", replacement)
24
+ replacement = sprintf("%#{filter.value}", replacement)
23
25
  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}"
26
+ message = <<~MSG
27
+ Invalid filter!
28
+ #{filter.inspect}
29
+ Verify it's either a valid native filter or is registered with Fmt.
30
+ Example: Fmt.add_filter(:#{filter.name}, &block)
31
+ #{error.message}
32
+ MSG
33
+ raise Fmt::Error, message
25
34
  end
26
35
  elsif filter.proc?
27
36
  begin
28
37
  replacement = filter.value.call(replacement)
29
38
  rescue => error
30
- raise Fmt::Error, "Error in filter! #{filter.inspect} #{error.message}"
39
+ message = <<~MSG
40
+ Error in filter!
41
+ #{filter.inspect}
42
+ #{error.message}
43
+ MSG
44
+ raise Fmt::Error, message
31
45
  end
32
46
  end
33
47
  end
34
48
 
35
- string.sub placeholder, replacement
49
+ result = string.sub(placeholder, replacement)
50
+ defined?(Rainbow) ? Rainbow(result) : result
51
+ end
52
+
53
+ private
54
+
55
+ def transform_embeds(string, **locals)
56
+ while embeds.any?
57
+ embed = embeds.shift
58
+ string = string.sub(embed.placeholder, embed.format(**locals))
59
+ end
60
+ string
36
61
  end
37
62
  end
38
63
  end
data/lib/fmt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fmt
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/fmt.rb CHANGED
@@ -11,9 +11,15 @@ module Fmt
11
11
  Formatter.instance
12
12
  end
13
13
 
14
+ def filters
15
+ formatter.filters
16
+ end
17
+
14
18
  def add_filter(...)
15
19
  formatter.filters.add(...)
16
20
  end
21
+
22
+ alias_method :add, :add_filter
17
23
  end
18
24
  end
19
25
 
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.1
4
+ version: 0.1.2
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-26 00:00:00.000000000 Z
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,15 @@ 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
164
  - lib/fmt/filters.rb
150
165
  - lib/fmt/formatter.rb
166
+ - lib/fmt/scanners.rb
167
+ - lib/fmt/scanners/base_scanner.rb
168
+ - lib/fmt/scanners/embed_scanner.rb
169
+ - lib/fmt/scanners/filter_scanner.rb
170
+ - lib/fmt/scanners/key_scanner.rb
151
171
  - lib/fmt/transformer.rb
152
172
  - lib/fmt/version.rb
153
173
  homepage: https://github.com/hopsoft/fmt
@@ -171,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
171
191
  - !ruby/object:Gem::Version
172
192
  version: '0'
173
193
  requirements: []
174
- rubygems_version: 3.5.5
194
+ rubygems_version: 3.5.11
175
195
  signing_key:
176
196
  specification_version: 4
177
197
  summary: A simple template engine based on native Ruby String formatting mechanics