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 +4 -4
- data/README.md +108 -89
- data/lib/fmt/parsers/embed_parser.rb +5 -12
- data/lib/fmt/parsers/macro_parser.rb +2 -11
- data/lib/fmt/parsers/template_parser.rb +84 -47
- data/lib/fmt/refinements/kernel_refinement.rb +2 -7
- data/lib/fmt/renderer.rb +39 -49
- data/lib/fmt/version.rb +1 -1
- data/sig/generated/fmt/parsers/embed_parser.rbs +1 -7
- data/sig/generated/fmt/parsers/macro_parser.rbs +0 -5
- data/sig/generated/fmt/parsers/template_parser.rbs +16 -10
- data/sig/generated/fmt/renderer.rbs +12 -19
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5cab8d0eef24b2446c187520bd8a8ac2b71cef4149ff6d0370ffd04d67120d1
|
4
|
+
data.tar.gz: 3e5b6dca4adf86661850769bd7ca0191fe7f9b62b7c7c8612765bb5c5fb54c0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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-
|
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
|
-
#
|
16
|
+
# CLI Templating System and String Formatter
|
17
17
|
|
18
|
-
|
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
|
-
- [
|
25
|
-
- [
|
26
|
-
|
27
|
-
- [
|
28
|
-
- [
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
##
|
42
|
+
## Getting Started
|
40
43
|
|
41
|
-
|
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,
|
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,
|
54
|
+
require "rainbow" # <- optional, for color support
|
60
55
|
require "fmt"
|
61
56
|
```
|
62
57
|
|
63
|
-
## Usage
|
58
|
+
## Usage
|
64
59
|
|
65
|
-
|
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"` -
|
68
|
-
- `"%{variable}"` -
|
69
|
-
- `"%<variable>s"` -
|
62
|
+
- `"%s"` - Standard format specifier
|
63
|
+
- `"%{variable}"` - Named format specifier
|
64
|
+
- `"%<variable>s"` - Named format specifier _(alternative syntax)_
|
70
65
|
|
71
|
-
|
66
|
+
### Macros
|
72
67
|
|
73
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
153
|
-
|
154
|
-
You can mix and match macros that target any type within a pipeline.
|
127
|
+
### Composition
|
155
128
|
|
156
|
-
Templates can
|
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
|
139
|
+
#### Embedded Templates
|
167
140
|
|
168
|
-
Embedded templates can be nested within other templates
|
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
|
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
|
188
|
+
### Customizing Fmt
|
216
189
|
|
217
|
-
|
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
|
-
|
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
|
-
##
|
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
|
-
|
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
|
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
|
-
|
274
|
+
Complex pipelines may take slightly longer to execute.
|
256
275
|
|
257
|
-
## Sponsors
|
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(
|
42
|
+
template = TemplateParser.new(source).parse
|
42
43
|
children = [key, placeholder, template].reject(&:empty?)
|
43
|
-
Node.new(:embed, children, urtext: urtext, source:
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
35
|
+
# 1) extract embeds first and update the source
|
36
|
+
embeds = extract_embeds(source)
|
40
37
|
embeds.each do |embed|
|
41
|
-
source =
|
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
|
69
|
-
# @rbs
|
70
|
-
|
71
|
-
|
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
|
-
|
74
|
-
|
106
|
+
rindex += 1
|
107
|
+
end
|
75
108
|
|
76
|
-
scanner =
|
77
|
-
|
109
|
+
scanner.pos = rindex
|
110
|
+
stack.zero? ? string[index...rindex] : nil
|
111
|
+
end
|
78
112
|
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
88
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
47
|
-
def render_embeds(
|
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
|
58
|
-
def render_pipelines(
|
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
|
-
|
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 =
|
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
|
-
|
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
@@ -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: (
|
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
|
-
|
6
|
+
EMBED_PEEK: ::Regexp
|
7
7
|
|
8
|
-
|
8
|
+
PIPELINE_HEAD: ::Regexp
|
9
9
|
|
10
|
-
|
10
|
+
PIPELINE_PEEK: ::Regexp
|
11
11
|
|
12
|
-
|
12
|
+
PIPELINE_TAIL: ::Regexp
|
13
13
|
|
14
14
|
# Constructor
|
15
15
|
# @rbs urtext: String -- original source code
|
16
|
-
|
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
|
-
#
|
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
|
33
|
-
def render_embeds: (
|
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
|
40
|
-
def render_pipelines: (
|
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.
|
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-
|
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:
|
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.
|
304
|
+
rubygems_version: 3.5.21
|
305
305
|
signing_key:
|
306
306
|
specification_version: 4
|
307
|
-
summary:
|
307
|
+
summary: CLI Templating System and String Formatter
|
308
308
|
test_files: []
|