fmt 0.1.1 → 0.1.2

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 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