fmt 0.3.0 → 0.3.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: c91008418cc9c29be346581fda313f6a498203899c932480187bb9f70bf4fb3f
4
- data.tar.gz: 77925da927d90b09fdba3edcd595dc392f43286d4e8468792038bd1f57242cd7
3
+ metadata.gz: c5cab8d0eef24b2446c187520bd8a8ac2b71cef4149ff6d0370ffd04d67120d1
4
+ data.tar.gz: 3e5b6dca4adf86661850769bd7ca0191fe7f9b62b7c7c8612765bb5c5fb54c0b
5
5
  SHA512:
6
- metadata.gz: 7c440974ee5dcf7a3ef53cbbccf021f4011890f9f2d279e91aa9affb08f9032b034d9e39e3385b40a7e3988c399d0a96dff074581b8ac2ea037f081b6877fe0c
7
- data.tar.gz: f45953ec9a6095cdf98106ca5582a24631ff2262efef979c7e5cacbd0abaf686cd59a814aa217428e87d2064060e8b5af1aa5ec97b8d40124fab65e124841a84
6
+ metadata.gz: ceeb6ab3f6d47a8863a35b90addf52135d31905be200c402819a516fa2600bbc0672e03d813555031bee131c865d44a92f3ad431863a3a4efd72ebe7ed3b7cc3
7
+ data.tar.gz: cfdbbdb355a735235b7417bc5b96a636c7e4392d8911178fbaa80e16a7127db41d388101cbdb8e14aa15c1562846d83a8546b8137c35cca3ade24aa695e39435
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
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-1039-47d299.svg" />
3
+ <img alt="Lines of Code" src="https://img.shields.io/badge/loc-1042-47d299.svg" />
4
4
  </a>
5
5
  <a href="https://github.com/testdouble/standard">
6
6
  <img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
@@ -13,66 +13,59 @@
13
13
  </a>
14
14
  </p>
15
15
 
16
- # Fmt: Your Friendly String Formatter
16
+ # CLI Templating System and String Formatter
17
17
 
18
- Welcome to Fmt, your go-to companion for Ruby string templating simple, versatile, and surprisingly powerful!
18
+ **Fmt** is a powerful and flexible templating system and string formatter for Ruby, designed to streamline the creation of command-line interfaces and enhance general-purpose string formatting.
19
19
 
20
20
  <!-- Tocer[start]: Auto-generated, don't remove. -->
21
21
 
22
22
  ## Table of Contents
23
23
 
24
- - [What's Fmt, and why should you care?](#whats-fmt-and-why-should-you-care)
25
- - [Getting Started: It's a Breeze!](#getting-started-its-a-breeze)
26
- - [Usage: Your Formatting Adventure Begins](#usage-your-formatting-adventure-begins)
27
- - [Macros: The Secret Sauce](#macros-the-secret-sauce)
28
- - [Pipelines: Unleashing the Power of Chained Macros](#pipelines-unleashing-the-power-of-chained-macros)
29
- - [Supported Methods: The Ultimate Answer to Formatting](#supported-methods-the-ultimate-answer-to-formatting)
30
- - [Rainbow GEM: The Color of Magic](#rainbow-gem-the-color-of-magic)
31
- - [Composition: The Art of the Template](#composition-the-art-of-the-template)
32
- - [Embedded Templates: Nesting with Infinite Possibilities](#embedded-templates-nesting-with-infinite-possibilities)
33
- - [Customizing Fmt: Create Your Own Extensions](#customizing-fmt-create-your-own-extensions)
34
- - [Performance: Fast Formatting](#performance-fast-formatting)
35
- - [Sponsors: Our Awesome Supporters](#sponsors-our-awesome-supporters)
24
+ - [Getting Started](#getting-started)
25
+ - [Usage](#usage)
26
+ - [Macros](#macros)
27
+ - [Pipelines](#pipelines)
28
+ - [Supported Methods](#supported-methods)
29
+ - [Rainbow GEM](#rainbow-gem)
30
+ - [Composition](#composition)
31
+ - [Embedded Templates](#embedded-templates)
32
+ - [Customizing Fmt](#customizing-fmt)
33
+ - [Kernel Refinement](#kernel-refinement)
34
+ - [`fmt(object, *pipeline)`](#fmtobject-pipeline)
35
+ - [`fmt_print(object, *pipeline)`](#fmt_printobject-pipeline)
36
+ - [`fmt_puts(object, *pipeline)`](#fmt_putsobject-pipeline)
37
+ - [Performance](#performance)
38
+ - [Sponsors](#sponsors)
36
39
 
37
40
  <!-- Tocer[finish]: Auto-generated, don't remove. -->
38
41
 
39
- ## What's Fmt, and why should you care?
42
+ ## Getting Started
40
43
 
41
- Fmt is a template engine that harnesses the raw power of Ruby's native string formatting mechanics. It's like having a universal translator for your strings, turning your formatting wishes into beautiful, functional output.
42
-
43
- - 🚀 Supercharge your general-purpose templating
44
- - 🎨 Craft CLI applications so beautiful, they'll make even the most stoic developer smile
45
- - 🧠 Intuitive enough for beginners, powerful enough for experts
46
-
47
- ## Getting Started: It's a Breeze!
48
-
49
- First, let's get you set up. It's easier than making a cup of coffee!
44
+ Install the required dependencies:
50
45
 
51
46
  ```sh
52
- bundle add rainbow # <- optional, but recommended for those who enjoy a splash of color
47
+ bundle add rainbow # <- optional, for color support
53
48
  bundle add fmt
54
49
  ```
55
50
 
56
- Then, in your Ruby file:
51
+ Then, require the necessary libraries in your Ruby file:
57
52
 
58
53
  ```ruby
59
- require "rainbow" # <- optional, but why not add some color to your life?
54
+ require "rainbow" # <- optional, for color support
60
55
  require "fmt"
61
56
  ```
62
57
 
63
- ## Usage: Your Formatting Adventure Begins
58
+ ## Usage
64
59
 
65
- Using Fmt is simpler than ordering takeout. Just create a string with Ruby's [format specifiers](https://ruby-doc.org/3.3.5/format_specifications_rdoc.html), much like you would for `sprintf`.
60
+ Fmt uses Ruby's native [format specifiers](https://ruby-doc.org/3.3.5/format_specifications_rdoc.html) to create templates:
66
61
 
67
- - `"%s"` - The classic
68
- - `"%{variable}"` - For when you're feeling descriptive
69
- - `"%<variable>s"` - When you want to be extra explicit
62
+ - `"%s"` - Standard format specifier
63
+ - `"%{variable}"` - Named format specifier
64
+ - `"%<variable>s"` - Named format specifier _(alternative syntax)_
70
65
 
71
- Remember, with Fmt, any string can be a template. It's like having a Swiss Army knife for text formatting!
66
+ ### Macros
72
67
 
73
- ### Macros: The Secret Sauce
74
-
75
- Formatting macros are what make Fmt special. Append them to your format specifiers like so:
68
+ Formatting macros are appended to format specifiers to modify the output:
76
69
 
77
70
  <!-- test_e798c3 -->
78
71
 
@@ -81,7 +74,7 @@ Fmt("%s|>capitalize", "hello world!") # => "Hello world!"
81
74
  Fmt("%{msg}|>capitalize", msg: "hello world!") # => "Hello world!"
82
75
  ```
83
76
 
84
- Macros can accept arguments.
77
+ Macros can accept arguments:
85
78
 
86
79
  <!-- test_1707d2 -->
87
80
 
@@ -90,9 +83,9 @@ Fmt("%s|>prepend('Hello ')", "world!") # => "Hello world!"
90
83
  Fmt("%{msg}|>prepend('Hello ')", msg: "world!") # => "Hello world!"
91
84
  ```
92
85
 
93
- ### Pipelines: Unleashing the Power of Chained Macros
86
+ ### Pipelines
94
87
 
95
- Macros can be chained to create a formatting pipeline.
88
+ Macros can be chained to create a formatting pipeline:
96
89
 
97
90
  <!-- test_425625 -->
98
91
 
@@ -101,10 +94,9 @@ Fmt("%s|>prepend('Hello ')|>ljust(32, '.')|>upcase", "world!") # => "HELLO WORLD
101
94
  Fmt("%{msg}|>prepend('Hello ')|>ljust(32, '.')|>upcase", msg: "world!") # => "HELLO WORLD!...................."
102
95
  ```
103
96
 
104
- > [!NOTE]
105
- > Pipelines are processed left to right. The return value from the preceeding macro is the starting value for the next macro.
97
+ Pipelines are processed left to right, with the return value from the preceding macro serving as the starting value for the next macro.
106
98
 
107
- Arguments and return values can be any type.
99
+ Arguments and return values can be of any type:
108
100
 
109
101
  <!-- test_f55ae2 -->
110
102
 
@@ -112,34 +104,17 @@ Arguments and return values can be any type.
112
104
  Fmt("%p|>partition(/:/)|>last|>delete_suffix('>')", Object.new) # => "0x000000011f33bc68"
113
105
  ```
114
106
 
115
- ### Supported Methods: The Ultimate Answer to Formatting
107
+ ### Supported Methods
116
108
 
117
- Most public instance methods on the following classes are supported.
109
+ Most public instance methods on the following classes are supported:
118
110
 
119
- - `Array`
120
- - `Date`
121
- - `DateTime`
122
- - `FalseClass`
123
- - `Float`
124
- - `Hash`
125
- - `Integer`
126
- - `NilClass`
127
- - `Range`
128
- - `Regexp`
129
- - `Set`
130
- - `StandardError`
131
- - `String`
132
- - `Struct`
133
- - `Symbol`
134
- - `Time`
135
- - `TrueClass`
111
+ `Array`, `Date`, `DateTime`, `FalseClass`, `Float`, `Hash`, `Integer`, `NilClass`, `Range`, `Regexp`, `Set`, `StandardError`, `String`, `Struct`, `Symbol`, `Time`, `TrueClass`
136
112
 
137
- > [!TIP]
138
- > If you're using libraries like ActiveSupport that extend these classes, extension methods will also available if the library is required before Fmt.
113
+ Extension methods from libraries like ActiveSupport will also be available if the library is required before Fmt.
139
114
 
140
- #### Rainbow GEM: The Color of Magic
115
+ #### Rainbow GEM
141
116
 
142
- Color and style support is available if your project includes the [Rainbow GEM](https://github.com/ku1ik/rainbow).
117
+ Color and style support is available if your project includes the [Rainbow GEM](https://github.com/ku1ik/rainbow):
143
118
 
144
119
  <!-- test_19c8ca -->
145
120
 
@@ -149,11 +124,9 @@ Fmt(template, msg: "Hello World!")
149
124
  #=> "\e[36m\e[1m\e[4mHello World!\e[0m"
150
125
  ```
151
126
 
152
- ### Composition: The Art of the Template
153
-
154
- You can mix and match macros that target any type within a pipeline.
127
+ ### Composition
155
128
 
156
- Templates can also include multiple format strings with their own distinct pipelines.
129
+ Templates can include multiple format strings with distinct pipelines:
157
130
 
158
131
  <!-- test_0dbfcd -->
159
132
 
@@ -163,9 +136,9 @@ Fmt(template, date: Time.now, msg: "this is cool")
163
136
  #=> "Date: \e[35m2024-09-20\e[0m \e[1mThis Is Cool\e[0m"
164
137
  ```
165
138
 
166
- #### Embedded Templates: Nesting with Infinite Possibilities
139
+ #### Embedded Templates
167
140
 
168
- Embedded templates can be nested within other templates... as deep as needed!
141
+ Embedded templates can be nested within other templates:
169
142
 
170
143
  <!-- test_efee7a -->
171
144
 
@@ -175,7 +148,7 @@ Fmt(template, msg: "Look Ma...", embed: "I'm embedded!")
175
148
  #=> "\e[2mLook Ma...\e[0m \e[1mI'm embedded!\e[0m"
176
149
  ```
177
150
 
178
- Embeds can also have their own distinct pipelines.
151
+ Embeds can have their own pipelines:
179
152
 
180
153
  <!-- test_abb7ea -->
181
154
 
@@ -185,7 +158,7 @@ Fmt(template, msg: "Look Ma...", embed: "I'm embedded!")
185
158
  #=> "\e[2mLook Ma...\e[0m \e[1m\e[4mI'm embedded!\e[0m"
186
159
  ```
187
160
 
188
- Embeds can be deeply nested.
161
+ Embeds can be deeply nested:
189
162
 
190
163
  <!-- test_79e924 -->
191
164
 
@@ -195,7 +168,7 @@ Fmt(template, msg: "Look Ma...", embed: "I'm embedded!", deep_embed: "And I'm de
195
168
  #=> "\e[2mLook Ma...\e[0m \e[1mI'm embedded!\e[0m \e[31m\e[1mAnd I'm deeply embedded!\e[0m"
196
169
  ```
197
170
 
198
- Embeds can also span multiple lines.
171
+ Embeds can also span multiple lines:
199
172
 
200
173
  <!-- test_054526 -->
201
174
 
@@ -212,9 +185,9 @@ Fmt(template, one: "Red", two: "Blue", three: "Green")
212
185
  #=> "Multiline:\n\e[31mRed\e[0m \e[1m\n \e[34mBlue\e[0m \n \e[32mGreen\e[0m"
213
186
  ```
214
187
 
215
- ### Customizing Fmt: Create Your Own Extensions
188
+ ### Customizing Fmt
216
189
 
217
- Want to add your own filters? It's easier than you might think:
190
+ Add custom filters by registering them with Fmt:
218
191
 
219
192
  <!-- test_2cacce -->
220
193
 
@@ -224,10 +197,7 @@ Fmt("%s|>shuffle", "This don't make no sense.")
224
197
  #=> "de.nnoTtsnh'oeek ssim a "
225
198
  ```
226
199
 
227
- You can also run a Ruby block with temporary filters if you don't want to officially register them.
228
-
229
- > [!TIP]
230
- > This also allows you to override existing filters for the duration of the block.
200
+ Run a Ruby block with temporary filters without officially registering them:
231
201
 
232
202
  <!-- test_7df4eb -->
233
203
 
@@ -241,20 +211,69 @@ Fmt("%s|>red", "This is original red!")
241
211
  #=> "\e[31mThis is original red!\e[0m"
242
212
  ```
243
213
 
244
- ## Performance: Fast Formatting
214
+ ## Kernel Refinement
215
+
216
+ Fmt provides a kernel refinement that adds convenient methods for formatting and outputting text directly. To use these methods, you need to enable the refinement in your code:
217
+
218
+ ```ruby
219
+ using Fmt::KernelRefinement
220
+ ```
221
+
222
+ Once enabled, you'll have access to the following methods:
223
+
224
+ ### `fmt(object, *pipeline)`
245
225
 
246
- Fmt isn't just pretty it's quick too!
226
+ This method formats an object using a different pipeline syntax:
227
+
228
+ ```ruby
229
+ fmt("Hello, World!", :bold) # => "\e[1mHello, World!\e[0m"
230
+ fmt(:hello, :underline) # => "\e[4mhello\e[0m"
231
+ fmt(Object.new, :red) # => "\e[31m#<Object:0x00007f9b8b0b0a08>\e[0m"
232
+ ```
233
+
234
+ ### `fmt_print(object, *pipeline)`
235
+
236
+ This method formats an object and prints it to STDOUT without a newline:
237
+
238
+ ```ruby
239
+ fmt_print("Hello, World!", :italic) # Prints: "\e[3mHello, World!\e[0m"
240
+ fmt_print(:hello, :green) # Prints: "\e[32mhello\e[0m"
241
+ ```
242
+
243
+ ### `fmt_puts(object, *pipeline)`
244
+
245
+ This method formats an object and prints it to STDOUT with a newline:
246
+
247
+ ```ruby
248
+ fmt_puts("Hello, World!", :bold, :underline) # Prints: "\e[1m\e[4mHello, World!\e[0m\n"
249
+ fmt_puts(:hello, :magenta) # Prints: "\e[35mhello\e[0m\n"
250
+ ```
251
+
252
+ These methods provide a convenient way to use Fmt's formatting capabilities directly in your code without explicitly calling the `Fmt` method.
253
+
254
+ You can pass any number of macros when using these methods:
255
+
256
+ ```ruby
257
+ fmt("Important!", :red, :bold, :underline)
258
+ # => "\e[31m\e[1m\e[4mImportant!\e[0m"
259
+
260
+ fmt_puts("Warning:", :yellow, :italic)
261
+ # Prints: "\e[33m\e[3mWarning:\e[0m\n"
262
+ ```
263
+
264
+ These kernel methods make it easy to integrate Fmt's powerful formatting capabilities into your command-line interfaces or any part of your Ruby application where you need to format and output text.
265
+
266
+ ## Performance
267
+
268
+ Fmt is optimized for performance:
247
269
 
248
270
  - Tokenization: Uses StringScanner and Ripper to parse and tokenize templates
249
271
  - Caching: Stores an Abstract Syntax Tree (AST) representation of each template, pipeline, and macro
250
- - Speed: Current benchmarks show an average pipeline execution time of **under <0.3 milliseconds**
251
-
252
- > [!NOTE]
253
- > While Fmt is optimized for performance, remember that complex pipelines might take a tad longer.
272
+ - Speed: Current benchmarks show an average pipeline execution time of under 0.3 milliseconds
254
273
 
255
- **Happy formatting!**
274
+ Complex pipelines may take slightly longer to execute.
256
275
 
257
- ## Sponsors: Our Awesome Supporters
276
+ ## Sponsors
258
277
 
259
278
  <p align="center">
260
279
  <em>Proudly sponsored by</em>
@@ -30,25 +30,18 @@ module Fmt
30
30
  # Extracts components for building the AST (Abstract Syntax Tree)
31
31
  # @rbs return: Hash[Symbol, Object] -- extracted components
32
32
  def extract
33
- {}
33
+ source = urtext.delete_prefix(Sigils::EMBED_PREFIX).delete_suffix(Sigils::EMBED_SUFFIX)
34
+ {source: source}
34
35
  end
35
36
 
36
37
  # Transforms extracted components into an AST (Abstract Syntax Tree)
37
38
  # @rbs return: Node -- AST (Abstract Syntax Tree)
38
- def transform(**)
39
+ def transform(source:)
39
40
  key = Node.new(:key, [self.key])
40
41
  placeholder = Node.new(:placeholder, [self.placeholder])
41
- template = TemplateParser.new(template_urtext).parse
42
+ template = TemplateParser.new(source).parse
42
43
  children = [key, placeholder, template].reject(&:empty?)
43
- Node.new(:embed, children, urtext: urtext, source: urtext)
44
- end
45
-
46
- private
47
-
48
- # Returns the template urtext
49
- # @rbs return: String
50
- def template_urtext
51
- urtext.delete_prefix(Sigils::EMBED_PREFIX).delete_suffix(Sigils::EMBED_SUFFIX)
44
+ Node.new(:embed, children, urtext: urtext, source: source)
52
45
  end
53
46
  end
54
47
  end
@@ -24,9 +24,7 @@ module Fmt
24
24
  # Extracts components for building the AST (Abstract Syntax Tree)
25
25
  # @rbs return: Hash[Symbol, Object] -- extracted components
26
26
  def extract
27
- code = urtext
28
- code = "#{Sigils::FORMAT_METHOD}('#{urtext}')" if native_format_string?(urtext)
29
-
27
+ code = urtext.delete_prefix(Sigils::FORMAT_PREFIX)
30
28
  tokens = tokenize(code)
31
29
  method = tokens.find(&:method_name?)&.value&.to_sym
32
30
 
@@ -46,7 +44,7 @@ module Fmt
46
44
  # @rbs arguments_tokens: Array[Token] -- arguments tokens
47
45
  # @rbs return: Node -- AST (Abstract Syntax Tree)
48
46
  def transform(method:, arguments_tokens:)
49
- method = Node.new(:name, [method], urtext: urtext, source: method)
47
+ method = Node.new(:name, [method], urtext: urtext, source: method.to_s)
50
48
  arguments = ArgumentsParser.new(arguments_tokens).parse
51
49
  source = "#{method.source}#{arguments.source}"
52
50
  children = [method, arguments].reject(&:empty?)
@@ -102,12 +100,5 @@ module Fmt
102
100
  return false if arguments_started?(tokens) && !arguments_finished?(tokens)
103
101
  true
104
102
  end
105
-
106
- # Indicates if a value is a Ruby native format string
107
- # @rbs value: String -- value to check
108
- # @rbs return: bool
109
- def native_format_string?(value)
110
- value.start_with? Sigils::FORMAT_PREFIX
111
- end
112
103
  end
113
104
  end
@@ -5,22 +5,18 @@
5
5
  module Fmt
6
6
  # Parses a template from a string and builds an AST (Abstract Syntax Tree)
7
7
  class TemplateParser < Parser
8
- PIPELINE_HEAD = %r{(?=(?!#{esc Sigils::PIPE_OPERATOR})#{Sigils::FORMAT_PREFIX})}o # : Regexp -- detects a native Ruby format string
9
- PIPELINE_TAIL = %r{(?=(\s+#{Sigils::FORMAT_PREFIX})|\z)}o # : Regexp -- detects a pipeline suffix
10
-
11
- EMBED_HEAD = %r{(?=#{esc Sigils::EMBED_PREFIX})}o # : Regexp -- detects an embed prefix
12
- EMBED_TAIL = %r{#{esc Sigils::EMBED_SUFFIX}}o # : Regexp -- detects an embed suffix
8
+ EMBED_PEEK = %r{(?=#{esc Sigils::EMBED_PREFIX})}o # : Regexp -- detects start of an embed prefix (look ahead)
9
+ PIPELINE_HEAD = %r{[#{Sigils::FORMAT_PREFIX}]}o # : Regexp -- detects start of a pipeline (i.e. Ruby format string)
10
+ PIPELINE_PEEK = %r{(?=[#{Sigils::FORMAT_PREFIX}][^#{Sigils::FORMAT_PREFIX}]|\z)}o # : Regexp -- detects start of a pipeline (look ahead)
11
+ PIPELINE_TAIL = %r{\s|\z}o # : Regexp -- detects end of a pipeline
13
12
 
14
13
  # Constructor
15
14
  # @rbs urtext: String -- original source code
16
- # @rbs scanner: StringScanner?
17
- def initialize(urtext = "", scanner: nil)
15
+ def initialize(urtext = "")
18
16
  @urtext = urtext.to_s
19
- @scanner = scanner || StringScanner.new(@urtext)
20
17
  end
21
18
 
22
19
  attr_reader :urtext # : String -- original source code
23
- attr_reader :scanner # : StringScanner?
24
20
 
25
21
  # Parses the urtext (original source code)
26
22
  # @rbs return: Node -- AST (Abstract Syntax Tree)
@@ -36,11 +32,13 @@ module Fmt
36
32
  def extract
37
33
  source = urtext
38
34
 
39
- embeds = extract_embeds
35
+ # 1) extract embeds first and update the source
36
+ embeds = extract_embeds(source)
40
37
  embeds.each do |embed|
41
- source = "#{source[0...embed[:index]]}#{embed[:placeholder]}#{source[embed[:rindex]..]}"
38
+ source = source.sub(embed[:urtext], embed[:placeholder])
42
39
  end
43
40
 
41
+ # 2) extract pipelines
44
42
  pipelines = extract_pipelines(source)
45
43
 
46
44
  {embeds: embeds, pipelines: pipelines, source: source}
@@ -65,61 +63,100 @@ module Fmt
65
63
 
66
64
  private
67
65
 
68
- # Extracts embed metadata from the urtext
69
- # @rbs return: Array[Hash] -- extracted embeds
70
- def extract_embeds
71
- embeds = []
66
+ # Extracts the next embed with the scanner
67
+ # @rbs scanner: StringScanner -- scanner to extract from
68
+ # @rbs return: String? -- extracted embed
69
+ # def extract_next_embed(scanner)
70
+ # return nil unless scanner.skip_until(EMBED_PEEK)
71
+
72
+ # index = scanner.pos
73
+ # rindex = index
74
+ # stack = 0
75
+ # string = scanner.string
76
+
77
+ # while rindex < string.length
78
+ # char = string.getbyte(rindex)
79
+
80
+ # case char
81
+ # when Sigils::EMBED_PREFIX.ord then stack += 1
82
+ # when Sigils::EMBED_SUFFIX.ord then stack -= 1
83
+ # end
84
+
85
+ # rindex += 1
86
+ # break if stack.zero?
87
+ # end
88
+
89
+ # stack.zero? ? string[index...rindex] : nil
90
+ # end
91
+ def extract_next_embed(scanner)
92
+ return nil unless scanner.skip_until(EMBED_PEEK)
93
+
94
+ string = scanner.string
95
+ index = scanner.pos
96
+ rindex = index
97
+ stack = 0
98
+
99
+ while rindex < string.length
100
+ case string[rindex]
101
+ in Sigils::EMBED_PREFIX[0] then break if (stack += 1).zero?
102
+ in Sigils::EMBED_SUFFIX[0] then break if (stack -= 1).zero?
103
+ else # noop
104
+ end
72
105
 
73
- index = nil
74
- embed = ""
106
+ rindex += 1
107
+ end
75
108
 
76
- scanner = StringScanner.new(urtext)
77
- scanner.skip_until(EMBED_HEAD)
109
+ scanner.pos = rindex
110
+ stack.zero? ? string[index...rindex] : nil
111
+ end
78
112
 
79
- while scanner.matched?
80
- index ||= scanner.charpos
81
- embed = "#{embed}#{scanner.scan_until(EMBED_TAIL)}"
113
+ # Extracts embed metadata from the source
114
+ # @rbs return: Array[Hash] -- extracted embeds
115
+ def extract_embeds(source)
116
+ scanner = StringScanner.new(source)
82
117
 
83
- if embed.scan(EMBED_HEAD).size == embed.scan(EMBED_TAIL).size
84
- rindex = scanner.charpos
85
- key = :"embed_#{index}_#{rindex}"
118
+ # will iterate until extract_next_embed returns nil... when run
119
+ generator = Enumerator.new do |yielder|
120
+ while (embed = extract_next_embed(scanner))
121
+ yielder << embed
122
+ end
123
+ end
86
124
 
87
- embeds << {
88
- index: index,
89
- rindex: rindex,
90
- key: key,
91
- placeholder: "#{Sigils::FORMAT_PREFIX}#{Sigils::KEY_PREFIXES[-1]}#{key}#{Sigils::KEY_SUFFIXES[-1]}",
92
- urtext: embed
93
- }
125
+ # runs the generator and returns the resulting array
126
+ embeds = generator.to_a
94
127
 
95
- index = nil
96
- embed = scanner.skip_until(EMBED_HEAD)
97
- end
128
+ embeds.map.with_index do |embed, index|
129
+ key = :"embed_#{index}"
130
+ placeholder = "#{Sigils::FORMAT_PREFIX}#{Sigils::KEY_PREFIXES[-1]}#{key}#{Sigils::KEY_SUFFIXES[-1]}"
131
+ {key: key, placeholder: placeholder, urtext: embed}
98
132
  end
133
+ end
99
134
 
100
- embeds
135
+ # Extracts the next pipeline with the scanner
136
+ # @rbs scanner: StringScanner -- scanner to extract from
137
+ # @rbs return: String? -- extracted pipeline
138
+ def extract_next_pipeline(scanner)
139
+ return nil unless scanner.skip_until(PIPELINE_HEAD)
140
+
141
+ index = scanner.pos
142
+ rindex = scanner.skip_until(PIPELINE_PEEK) ? scanner.pos : scanner.terminate.pos
143
+ pipeline = scanner.string[index - 1...rindex].strip
144
+ pipeline.rpartition(PIPELINE_TAIL).first
101
145
  end
102
146
 
103
147
  # Extracts pipelines from the source
104
148
  # @rbs source: String -- source code to extract pipelines from
105
149
  # @rbs return: Array[String] -- extracted pipelines
106
150
  def extract_pipelines(source)
107
- pipelines = []
108
- pipeline = ""
109
-
110
151
  scanner = StringScanner.new(source)
111
- scanner.skip_until(PIPELINE_HEAD)
112
-
113
- while scanner.matched?
114
- pipeline = scanner.scan_until(PIPELINE_TAIL)
115
152
 
116
- if scanner.matched?
117
- pipelines << pipeline
118
- scanner.skip_until(PIPELINE_HEAD)
153
+ generator = Enumerator.new do |yielder|
154
+ while (pipeline = extract_next_pipeline(scanner))
155
+ yielder << pipeline
119
156
  end
120
157
  end
121
158
 
122
- pipelines
159
+ generator.to_a
123
160
  end
124
161
  end
125
162
  end
@@ -10,12 +10,7 @@ module Fmt
10
10
  # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
11
11
  # @rbs return [String] -- formatted text
12
12
  def fmt(object, *pipeline)
13
- text = case object
14
- in String then object
15
- in Symbol then object.to_s
16
- else object.inspect
17
- end
18
- Fmt "%s|>to_s|>#{pipeline.join("|>")}", text
13
+ Fmt pipeline.prepend("%s").join(Sigils::PIPE_OPERATOR), object
19
14
  end
20
15
 
21
16
  # Formats an object with Fmt and prints to STDOUT
@@ -23,7 +18,7 @@ module Fmt
23
18
  # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
24
19
  # @rbs return void
25
20
  def fmt_print(object, *pipeline)
26
- puts fmt(object, *pipeline)
21
+ print fmt(object, *pipeline)
27
22
  end
28
23
 
29
24
  # Formats an object with Fmt and puts to STDOUT
data/lib/fmt/renderer.rb CHANGED
@@ -23,45 +23,37 @@ module Fmt
23
23
  def render(*args, **kwargs)
24
24
  raise Error, "positional and keyword arguments are mutually exclusive" if args.any? && kwargs.any?
25
25
 
26
- context = template.source
27
-
28
- render_embeds(context, *args, **kwargs) do |embed, result|
26
+ render_embeds(*args, **kwargs) do |embed, result|
29
27
  kwargs[embed.key] = result
30
28
  end
31
29
 
32
- render_pipelines(context, *args, **kwargs)
30
+ rendered = template.source
31
+ render_pipelines(*args, **kwargs) do |pipeline, result|
32
+ rendered = rendered.sub(pipeline.urtext, result.to_s)
33
+ end
34
+ rendered
33
35
  end
34
36
 
35
37
  private
36
38
 
37
- # Escapes a string for use in a regular expression
38
- # @rbs value: String -- string to escape
39
- # @rbs return: String -- escaped string
40
- def esc(value) = Regexp.escape(value.to_s)
41
-
42
39
  # Renders all template embeds
43
- # @rbs context: String -- starting context
44
40
  # @rbs args: Array[Object] -- positional arguments (user provided)
45
41
  # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
46
- # @rbs &block: Proc -- block to execute after rendering embeds (signature: Proc(String, *args, **kwargs))
47
- def render_embeds(context, *args, **kwargs)
42
+ # @rbs &block: Proc -- block executed for each embed (signature: Proc(Embed, String))
43
+ def render_embeds(*args, **kwargs)
48
44
  template.embeds.each do |embed|
49
45
  yield embed, Renderer.new(embed.template).render(*args, **kwargs)
50
46
  end
51
47
  end
52
48
 
53
49
  # Renders all template pipelines
54
- # @rbs context: String -- starting context
55
50
  # @rbs args: Array[Object] -- positional arguments (user provided)
56
51
  # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
57
- # @rbs return: String
58
- def render_pipelines(context, *args, **kwargs)
52
+ # @rbs block: Proc -- block executed for each pipeline (signature: Proc(Pipeline, String))
53
+ def render_pipelines(*args, **kwargs)
59
54
  template.pipelines.each_with_index do |pipeline, index|
60
- result = render_pipeline(pipeline, *args[index..], **kwargs)
61
- context = context.sub(pipeline.urtext, result)
55
+ yield pipeline, render_pipeline(pipeline, *args[index..], **kwargs)
62
56
  end
63
-
64
- context
65
57
  end
66
58
 
67
59
  # Renders a single pipeline
@@ -70,55 +62,53 @@ module Fmt
70
62
  # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
71
63
  # @rbs return: String
72
64
  def render_pipeline(pipeline, *args, **kwargs)
73
- result = ""
65
+ result = nil
74
66
 
75
67
  pipeline.macros.each do |macro|
76
- result = case macro
77
- in name: Sigils::FORMAT_METHOD
78
- case [args, kwargs]
79
- in [], {} then invoke_formatter(macro)
80
- in [], {**} => kwargs then invoke_formatter(macro, **kwargs)
81
- in [*], {} then invoke_formatter(macro, *args)
82
- in [*], {**} => kwargs then invoke_formatter(macro, *args, **kwargs)
83
- end
84
- else invoke_macro(result, macro)
85
- end
68
+ result = invoke_macro(result, macro, *args, **kwargs)
86
69
  end
87
70
 
88
71
  result
89
72
  end
90
73
 
91
- # Invokes native Ruby string formatting
92
- # @rbs macro: Macro -- macro to use (source, arguments, etc.)
93
- # @rbs args: Array[Object] -- positional arguments (user provided)
94
- # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
95
- # @rbs return: String
96
- def invoke_formatter(macro, *args, **kwargs)
97
- callable = Fmt.registry[[Kernel, macro.name]]
98
- context = macro.arguments.args[0]
99
- context.instance_exec(*args, **kwargs, &callable)
100
- rescue => error
101
- raise_format_error(macro, *args, cause: error, **kwargs)
102
- end
103
-
104
74
  # Invokes a macro
105
75
  # @rbs context: Object -- self in callable (Proc)
106
76
  # @rbs macro: Macro -- macro to use (source, arguments, etc.)
77
+ # @rbs args: Array[Object] -- positional arguments (user provided)
78
+ # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
107
79
  # @rbs return: Object -- result
108
- def invoke_macro(context, macro)
80
+ def invoke_macro(context, macro, *args, **kwargs)
109
81
  callable = Fmt.registry[[context.class, macro.name]] || Fmt.registry[[Object, macro.name]]
110
- raise Error, "[#{context.class.name} | Object, #{macro.name}] is not a registered formatter!" unless callable
111
-
112
- args = macro.arguments.args
113
- kwargs = macro.arguments.kwargs
114
82
 
115
- context.instance_exec(*args, **kwargs, &callable)
83
+ case callable
84
+ in nil
85
+ if kwargs.key? macro.name
86
+ kwargs[macro.name]
87
+ else
88
+ quietly do
89
+ context.instance_exec { sprintf(macro.urtext, *args, **kwargs) }
90
+ end
91
+ end
92
+ else
93
+ context.instance_exec(*macro.arguments.args, **macro.arguments.kwargs, &callable)
94
+ end
116
95
  rescue => error
117
96
  args ||= []
118
97
  kwargs ||= {}
119
98
  raise_format_error(macro, *args, cause: error, **kwargs)
120
99
  end
121
100
 
101
+ # Suppresses verbose output for the duration of the block
102
+ # @rbs block: Proc -- block to execute
103
+ # @rbs return: void
104
+ def quietly
105
+ verbose = $VERBOSE
106
+ $VERBOSE = nil
107
+ yield
108
+ ensure
109
+ $VERBOSE = verbose
110
+ end
111
+
122
112
  # Raises an invocation error if/when Proc invocations fail
123
113
  # @rbs macro: Macro -- macro that failed
124
114
  # @rbs args: Array[Object] -- positional arguments (user provided)
data/lib/fmt/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  module Fmt
6
- VERSION = "0.3.0"
6
+ VERSION = "0.3.2"
7
7
  end
@@ -25,12 +25,6 @@ module Fmt
25
25
 
26
26
  # Transforms extracted components into an AST (Abstract Syntax Tree)
27
27
  # @rbs return: Node -- AST (Abstract Syntax Tree)
28
- def transform: (**untyped) -> Node
29
-
30
- private
31
-
32
- # Returns the template urtext
33
- # @rbs return: String
34
- def template_urtext: () -> String
28
+ def transform: (source: untyped) -> Node
35
29
  end
36
30
  end
@@ -51,10 +51,5 @@ module Fmt
51
51
  # @rbs tokens: Array[Token] -- tokens to check
52
52
  # @rbs return: bool
53
53
  def finished?: (Array[Token] tokens) -> bool
54
-
55
- # Indicates if a value is a Ruby native format string
56
- # @rbs value: String -- value to check
57
- # @rbs return: bool
58
- def native_format_string?: (String value) -> bool
59
54
  end
60
55
  end
@@ -3,23 +3,20 @@
3
3
  module Fmt
4
4
  # Parses a template from a string and builds an AST (Abstract Syntax Tree)
5
5
  class TemplateParser < Parser
6
- PIPELINE_HEAD: ::Regexp
6
+ EMBED_PEEK: ::Regexp
7
7
 
8
- PIPELINE_TAIL: ::Regexp
8
+ PIPELINE_HEAD: ::Regexp
9
9
 
10
- EMBED_HEAD: ::Regexp
10
+ PIPELINE_PEEK: ::Regexp
11
11
 
12
- EMBED_TAIL: ::Regexp
12
+ PIPELINE_TAIL: ::Regexp
13
13
 
14
14
  # Constructor
15
15
  # @rbs urtext: String -- original source code
16
- # @rbs scanner: StringScanner?
17
- def initialize: (?String urtext, ?scanner: StringScanner?) -> untyped
16
+ def initialize: (?String urtext) -> untyped
18
17
 
19
18
  attr_reader urtext: untyped
20
19
 
21
- attr_reader scanner: untyped
22
-
23
20
  # Parses the urtext (original source code)
24
21
  # @rbs return: Node -- AST (Abstract Syntax Tree)
25
22
  def parse: () -> Node
@@ -38,9 +35,18 @@ module Fmt
38
35
 
39
36
  private
40
37
 
41
- # Extracts embed metadata from the urtext
38
+ # stack.zero? ? string[index...rindex] : nil
39
+ # end
40
+ def extract_next_embed: (untyped scanner) -> untyped
41
+
42
+ # Extracts embed metadata from the source
42
43
  # @rbs return: Array[Hash] -- extracted embeds
43
- def extract_embeds: () -> Array[Hash]
44
+ def extract_embeds: (untyped source) -> Array[Hash]
45
+
46
+ # Extracts the next pipeline with the scanner
47
+ # @rbs scanner: StringScanner -- scanner to extract from
48
+ # @rbs return: String? -- extracted pipeline
49
+ def extract_next_pipeline: (StringScanner scanner) -> String?
44
50
 
45
51
  # Extracts pipelines from the source
46
52
  # @rbs source: String -- source code to extract pipelines from
@@ -20,24 +20,17 @@ module Fmt
20
20
 
21
21
  private
22
22
 
23
- # Escapes a string for use in a regular expression
24
- # @rbs value: String -- string to escape
25
- # @rbs return: String -- escaped string
26
- def esc: (String value) -> String
27
-
28
23
  # Renders all template embeds
29
- # @rbs context: String -- starting context
30
24
  # @rbs args: Array[Object] -- positional arguments (user provided)
31
25
  # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
32
- # @rbs &block: Proc -- block to execute after rendering embeds (signature: Proc(String, *args, **kwargs))
33
- def render_embeds: (String context, *untyped args, **untyped kwargs) -> untyped
26
+ # @rbs &block: Proc -- block executed for each embed (signature: Proc(Embed, String))
27
+ def render_embeds: (*untyped args, **untyped kwargs) -> untyped
34
28
 
35
29
  # Renders all template pipelines
36
- # @rbs context: String -- starting context
37
30
  # @rbs args: Array[Object] -- positional arguments (user provided)
38
31
  # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
39
- # @rbs return: String
40
- def render_pipelines: (String context, *untyped args, **untyped kwargs) -> String
32
+ # @rbs block: Proc -- block executed for each pipeline (signature: Proc(Pipeline, String))
33
+ def render_pipelines: (*untyped args, **untyped kwargs) -> untyped
41
34
 
42
35
  # Renders a single pipeline
43
36
  # @rbs pipeline: Pipeline -- pipeline to render
@@ -46,18 +39,18 @@ module Fmt
46
39
  # @rbs return: String
47
40
  def render_pipeline: (Pipeline pipeline, *untyped args, **untyped kwargs) -> String
48
41
 
49
- # Invokes native Ruby string formatting
50
- # @rbs macro: Macro -- macro to use (source, arguments, etc.)
51
- # @rbs args: Array[Object] -- positional arguments (user provided)
52
- # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
53
- # @rbs return: String
54
- def invoke_formatter: (Macro macro, *untyped args, **untyped kwargs) -> String
55
-
56
42
  # Invokes a macro
57
43
  # @rbs context: Object -- self in callable (Proc)
58
44
  # @rbs macro: Macro -- macro to use (source, arguments, etc.)
45
+ # @rbs args: Array[Object] -- positional arguments (user provided)
46
+ # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
59
47
  # @rbs return: Object -- result
60
- def invoke_macro: (Object context, Macro macro) -> Object
48
+ def invoke_macro: (Object context, Macro macro, *untyped args, **untyped kwargs) -> Object
49
+
50
+ # Suppresses verbose output for the duration of the block
51
+ # @rbs block: Proc -- block to execute
52
+ # @rbs return: void
53
+ def quietly: () -> void
61
54
 
62
55
  # Raises an invocation error if/when Proc invocations fail
63
56
  # @rbs macro: Macro -- macro that failed
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.3.0
4
+ version: 0.3.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-09-22 00:00:00.000000000 Z
11
+ date: 2024-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ast
@@ -220,7 +220,7 @@ dependencies:
220
220
  - - ">="
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0'
223
- description: Pipeline driven template engine based on native Ruby String formatting
223
+ description: CLI Templating System and String Formatter
224
224
  email:
225
225
  - natehop@gmail.com
226
226
  executables: []
@@ -301,8 +301,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
301
301
  - !ruby/object:Gem::Version
302
302
  version: '0'
303
303
  requirements: []
304
- rubygems_version: 3.5.16
304
+ rubygems_version: 3.5.21
305
305
  signing_key:
306
306
  specification_version: 4
307
- summary: Pipeline driven template engine based on native Ruby String formatting
307
+ summary: CLI Templating System and String Formatter
308
308
  test_files: []