├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── .standard.yml
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
├── benchmark
├── console
├── format
├── loc
├── rbs
├── setup
└── toc
├── fmt.gemspec
├── lib
├── fmt.rb
└── fmt
│ ├── boot.rb
│ ├── lru_cache.rb
│ ├── mixins
│ └── matchable.rb
│ ├── models
│ ├── arguments.rb
│ ├── embed.rb
│ ├── macro.rb
│ ├── model.rb
│ ├── pipeline.rb
│ └── template.rb
│ ├── node.rb
│ ├── parsers
│ ├── arguments_parser.rb
│ ├── embed_parser.rb
│ ├── macro_parser.rb
│ ├── parser.rb
│ ├── pipeline_parser.rb
│ └── template_parser.rb
│ ├── refinements
│ └── kernel_refinement.rb
│ ├── registries
│ ├── native_registry.rb
│ ├── rainbow_registry.rb
│ └── registry.rb
│ ├── renderer.rb
│ ├── sigils.rb
│ ├── token.rb
│ ├── tokenizer.rb
│ └── version.rb
├── sig
└── generated
│ ├── fmt.rbs
│ └── fmt
│ ├── boot.rbs
│ ├── lru_cache.rbs
│ ├── mixins
│ └── matchable.rbs
│ ├── models
│ ├── arguments.rbs
│ ├── embed.rbs
│ ├── macro.rbs
│ ├── model.rbs
│ ├── pipeline.rbs
│ └── template.rbs
│ ├── node.rbs
│ ├── parsers
│ ├── arguments_parser.rbs
│ ├── embed_parser.rbs
│ ├── macro_parser.rbs
│ ├── parser.rbs
│ ├── pipeline_parser.rbs
│ └── template_parser.rbs
│ ├── refinements
│ └── kernel_refinement.rbs
│ ├── registries
│ ├── native_registry.rbs
│ ├── rainbow_registry.rbs
│ └── registry.rbs
│ ├── renderer.rbs
│ ├── sigils.rbs
│ ├── token.rbs
│ ├── tokenizer.rbs
│ └── version.rbs
└── test
├── refinements
└── test_kernel_refinement.rb
├── test_fmt.rb
├── test_helper.rb
├── test_lru_cache.rb
├── test_native_formatters.rb
├── test_rainbow_formatters.rb
└── test_readme.rb
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-22.04
12 | env:
13 | TERM: xterm-256color
14 | BM: "true"
15 | strategy:
16 | matrix:
17 | ruby-version: ["3.3"]
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 |
22 | - name: Set up Ruby ${{ matrix.ruby-version }}
23 | uses: ruby/setup-ruby@v1
24 | with:
25 | ruby-version: ${{ matrix.ruby-version }}
26 | bundler-cache: true
27 |
28 | - name: Install dependencies
29 | run: bundle install
30 |
31 | - name: Run tests
32 | run: bundle exec rake
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | Gemfile.lock
10 |
--------------------------------------------------------------------------------
/.standard.yml:
--------------------------------------------------------------------------------
1 | # For available configuration options, see: https://github.com/standardrb/standard
2 | ruby_version: 3.0
3 | fix: true
4 | parallel: true
5 | format: progress
6 | default_ignores: true
7 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | # Specify your gem's dependencies in fmt.gemspec
6 | gemspec
7 |
8 | gem "rake", "~> 13.0"
9 |
10 | gem "minitest", "~> 5.16"
11 |
12 | gem "standard", "~> 1.3"
13 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Nate Hopkins (hopsoft)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | # CLI Templating System and String Formatter
26 |
27 | **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.
28 |
29 |
30 |
31 | ## Table of Contents
32 |
33 | - [Getting Started](#getting-started)
34 | - [Usage](#usage)
35 | - [Macros](#macros)
36 | - [Pipelines](#pipelines)
37 | - [Supported Methods](#supported-methods)
38 | - [Rainbow GEM](#rainbow-gem)
39 | - [Composition](#composition)
40 | - [Embedded Templates](#embedded-templates)
41 | - [Customizing Fmt](#customizing-fmt)
42 | - [Kernel Refinement](#kernel-refinement)
43 | - [`fmt(object, *pipeline)`](#fmtobject-pipeline)
44 | - [`fmt_print(object, *pipeline)`](#fmt_printobject-pipeline)
45 | - [`fmt_puts(object, *pipeline)`](#fmt_putsobject-pipeline)
46 | - [Performance](#performance)
47 | - [Sponsors](#sponsors)
48 |
49 |
50 |
51 | ## Getting Started
52 |
53 | Install the required dependencies:
54 |
55 | ```sh
56 | bundle add rainbow # <- optional, for color support
57 | bundle add fmt
58 | ```
59 |
60 | Then, require the necessary libraries in your Ruby file:
61 |
62 | ```ruby
63 | require "rainbow" # <- optional, for color support
64 | require "fmt"
65 | ```
66 |
67 | ## Usage
68 |
69 | Fmt uses Ruby's native [format specifiers](https://ruby-doc.org/3.3.5/format_specifications_rdoc.html) to create templates:
70 |
71 | - `"%s"` - Standard format specifier
72 | - `"%{variable}"` - Named format specifier
73 | - `"%s"` - Named format specifier _(alternative syntax)_
74 |
75 | ### Macros
76 |
77 | Formatting macros are appended to format specifiers to modify the output:
78 |
79 |
80 |
81 | ```ruby
82 | Fmt("%s|>capitalize", "hello world!") # => "Hello world!"
83 | Fmt("%{msg}|>capitalize", msg: "hello world!") # => "Hello world!"
84 | ```
85 |
86 | Macros can accept arguments:
87 |
88 |
89 |
90 | ```ruby
91 | Fmt("%s|>prepend('Hello ')", "world!") # => "Hello world!"
92 | Fmt("%{msg}|>prepend('Hello ')", msg: "world!") # => "Hello world!"
93 | ```
94 |
95 | ### Pipelines
96 |
97 | Macros can be chained to create a formatting pipeline:
98 |
99 |
100 |
101 | ```ruby
102 | Fmt("%s|>prepend('Hello ')|>ljust(32, '.')|>upcase", "world!") # => "HELLO WORLD!...................."
103 | Fmt("%{msg}|>prepend('Hello ')|>ljust(32, '.')|>upcase", msg: "world!") # => "HELLO WORLD!...................."
104 | ```
105 |
106 | Pipelines are processed left to right, with the return value from the preceding macro serving as the starting value for the next macro.
107 |
108 | Arguments and return values can be of any type:
109 |
110 |
111 |
112 | ```ruby
113 | Fmt("%p|>partition(/:/)|>last|>delete_suffix('>')", Object.new) # => "0x000000011f33bc68"
114 | ```
115 |
116 | ### Supported Methods
117 |
118 | Most public instance methods on the following classes are supported:
119 |
120 | `Array`, `Date`, `DateTime`, `FalseClass`, `Float`, `Hash`, `Integer`, `NilClass`, `Range`, `Regexp`, `Set`, `StandardError`, `String`, `Struct`, `Symbol`, `Time`, `TrueClass`
121 |
122 | Extension methods from libraries like ActiveSupport will also be available if the library is required before Fmt.
123 |
124 | #### Rainbow GEM
125 |
126 | Color and style support is available if your project includes the [Rainbow GEM](https://github.com/ku1ik/rainbow):
127 |
128 |
129 |
130 | ```ruby
131 | template = "%{msg}|>cyan|>bold|>underline"
132 | Fmt(template, msg: "Hello World!")
133 | #=> "\e[36m\e[1m\e[4mHello World!\e[0m"
134 | ```
135 |
136 | ### Composition
137 |
138 | Templates can include multiple format strings with distinct pipelines:
139 |
140 |
141 |
142 | ```ruby
143 | template = "Date: %.10s|>magenta -- %{msg}|>titleize|>bold"
144 | Fmt(template, date: Time.now, msg: "this is cool")
145 | #=> "Date: \e[35m2024-09-21\e[0m -- \e[1mThis Is Cool\e[0m"
146 | ```
147 |
148 | #### Embedded Templates
149 |
150 | Embedded templates can be nested within other templates:
151 |
152 |
153 |
154 | ```ruby
155 | template = "%{msg}|>faint {{%{embed}|>bold}}"
156 | Fmt(template, msg: "Look Ma...", embed: "I'm embedded!")
157 | #=> "\e[2mLook Ma...\e[0m \e[1mI'm embedded!\e[0m"
158 | ```
159 |
160 | Embeds can have their own pipelines:
161 |
162 |
163 |
164 | ```ruby
165 | template = "%{msg}|>faint {{%{embed}|>bold}}|>underline"
166 | Fmt(template, msg: "Look Ma...", embed: "I'm embedded!")
167 | #=> "\e[2mLook Ma...\e[0m \e[1m\e[4mI'm embedded!\e[0m"
168 | ```
169 |
170 | Embeds can be deeply nested:
171 |
172 |
173 |
174 | ```ruby
175 | template = "%{msg}|>faint {{%{embed}|>bold {{%{deep_embed}|>red|>bold}}}}"
176 | Fmt(template, msg: "Look Ma...", embed: "I'm embedded!", deep_embed: "And I'm deeply embedded!")
177 | #=> "\e[2mLook Ma...\e[0m \e[1mI'm embedded!\e[0m \e[31m\e[1mAnd I'm deeply embedded!\e[0m"
178 | ```
179 |
180 | Embeds can also span multiple lines:
181 |
182 |
183 |
184 | ```ruby
185 | template = <<~T
186 | Multiline:
187 | %{one}|>red {{
188 | %{two}|>blue {{
189 | %{three}|>green
190 | }}|>bold
191 | }}
192 | T
193 | Fmt(template, one: "Red", two: "Blue", three: "Green")
194 | #=> "Multiline:\n\e[31mRed\e[0m \n \e[34mBlue\e[0m \e[1m\n \e[32mGreen\e[0m\n \e[0m\n\n"
195 | ```
196 |
197 | ### Customizing Fmt
198 |
199 | Add custom filters by registering them with Fmt:
200 |
201 |
202 |
203 | ```ruby
204 | Fmt.register([Object, :shuffle]) { |*args, **kwargs| to_s.chars.shuffle.join }
205 | Fmt("%s|>shuffle", "This don't make no sense.")
206 | #=> "de.nnoTtsnh'oeek ssim a "
207 | ```
208 |
209 | Run a Ruby block with temporary filters without officially registering them:
210 |
211 |
212 |
213 | ```ruby
214 | Fmt.with_overrides([Object, :red] => proc { |*args, **kwargs| Rainbow(self).crimson.bold }) do
215 | Fmt("%s|>red", "This is customized red!")
216 | #=> "\e[38;5;197m\e[1mThis is customized red!\e[0m"
217 | end
218 |
219 | Fmt("%s|>red", "This is original red!")
220 | #=> "\e[31mThis is original red!\e[0m"
221 | ```
222 |
223 | ## Kernel Refinement
224 |
225 | 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:
226 |
227 | ```ruby
228 | using Fmt::KernelRefinement
229 | ```
230 |
231 | Once enabled, you'll have access to the following methods:
232 |
233 | ### `fmt(object, *pipeline)`
234 |
235 | This method formats an object using a different pipeline syntax:
236 |
237 | ```ruby
238 | fmt("Hello, World!", :bold) # => "\e[1mHello, World!\e[0m"
239 | fmt(:hello, :underline) # => "\e[4mhello\e[0m"
240 | fmt(Object.new, :red) # => "\e[31m#\e[0m"
241 | ```
242 |
243 | ### `fmt_print(object, *pipeline)`
244 |
245 | This method formats an object and prints it to STDOUT without a newline:
246 |
247 | ```ruby
248 | fmt_print("Hello, World!", :italic) # Prints: "\e[3mHello, World!\e[0m"
249 | fmt_print(:hello, :green) # Prints: "\e[32mhello\e[0m"
250 | ```
251 |
252 | ### `fmt_puts(object, *pipeline)`
253 |
254 | This method formats an object and prints it to STDOUT with a newline:
255 |
256 | ```ruby
257 | fmt_puts("Hello, World!", :bold, :underline) # Prints: "\e[1m\e[4mHello, World!\e[0m\n"
258 | fmt_puts(:hello, :magenta) # Prints: "\e[35mhello\e[0m\n"
259 | ```
260 |
261 | These methods provide a convenient way to use Fmt's formatting capabilities directly in your code without explicitly calling the `Fmt` method.
262 |
263 | You can pass any number of macros when using these methods:
264 |
265 | ```ruby
266 | fmt("Important!", :red, :bold, :underline)
267 | # => "\e[31m\e[1m\e[4mImportant!\e[0m"
268 |
269 | fmt_puts("Warning:", :yellow, :italic)
270 | # Prints: "\e[33m\e[3mWarning:\e[0m\n"
271 | ```
272 |
273 | 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.
274 |
275 | ## Performance
276 |
277 | Fmt is optimized for performance:
278 |
279 | - Tokenization: Uses StringScanner and Ripper to parse and tokenize templates
280 | - Caching: Stores an Abstract Syntax Tree (AST) representation of each template, pipeline, and macro
281 | - Speed: Current benchmarks show an average pipeline execution time of under 0.3 milliseconds
282 |
283 | Complex pipelines may take slightly longer to execute.
284 |
285 | ## Sponsors
286 |
287 |
288 | Proudly sponsored by
289 |
290 |
291 |
292 |
293 |
294 |
295 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "minitest/test_task"
5 | require "standard/rake"
6 |
7 | Minitest::TestTask.create
8 |
9 | task default: %i[test]
10 |
--------------------------------------------------------------------------------
/bin/benchmark:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require_relative "../lib/fmt"
6 | require "benchmark"
7 | require "rainbow"
8 |
9 | source = "Multiple: %s|>truncate(length: 80, separator: '.')|>red|>bold %{a}green|>faint %blue|>italic|>strike %bold|>underline"
10 |
11 | first = Benchmark.measure { Fmt::TemplateParser.new(source).parse }
12 | puts Rainbow("".ljust(80, ".")).faint
13 | puts "#{Rainbow(" First Run:").red.bright} #{Rainbow(first.to_s.strip).orange} #{Rainbow("(compiles and caches templates)").italic.darkred}"
14 |
15 | 100.times do
16 | subsequent = Benchmark.measure { Fmt::TemplateParser.new(source).parse }
17 | times_faster = first.real / subsequent.real
18 | percentage_faster = ((first.real - subsequent.real) / first.real) * 100
19 | puts Rainbow("".ljust(80, ".")).faint
20 | message = format("%.2fx (%.1f%% faster)", times_faster, percentage_faster)
21 | puts "#{Rainbow("Subsequent Run:").green} #{Rainbow(subsequent.to_s.strip).green.bright}"
22 | puts "#{Rainbow(" Improvement:").green} #{Rainbow(message).lime.bold.underline}"
23 | puts
24 | end
25 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require "active_support/all"
6 | require "amazing_print"
7 | require "rainbow"
8 | require "pry-byebug"
9 | require "pry-doc"
10 | require_relative "../lib/fmt"
11 |
12 | AmazingPrint.pry!
13 | Pry.start
14 |
--------------------------------------------------------------------------------
/bin/format:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # frozen_string_literal: true
4 |
5 | frozen_string = "# frozen_string_literal: true"
6 | rbs_inline = "# rbs_inline: enabled"
7 |
8 | paths = Dir.glob(File.join(File.expand_path("../lib", __dir__), "**", "*.{rb}"))
9 | paths += Dir.glob(File.join(File.expand_path("../test", __dir__), "**", "*.{rb}"))
10 |
11 | paths.each do |path|
12 | lines = File.readlines(path)
13 |
14 | frozen = lines.any? { _1.start_with? frozen_string }
15 | rbs = lines.any? { _1.start_with? rbs_inline }
16 |
17 | case [frozen, rbs]
18 | in [true, true] then next
19 | in [false, true]
20 | lines
21 | .insert(0, "#{frozen_string}\n")
22 | .insert(1, "\n")
23 | in [true, false]
24 | lines
25 | .insert(1, "\n")
26 | .insert(2, "#{rbs_inline}\n")
27 | in [false, false]
28 | lines
29 | .insert(0, "#{frozen_string}\n")
30 | .insert(1, "\n")
31 | .insert(2, "#{rbs_inline}\n")
32 | .insert(3, "\n")
33 | end
34 |
35 | File.write(path, lines.join)
36 | end
37 |
38 | system "bundle exec standardrb --fix ."
39 |
--------------------------------------------------------------------------------
/bin/loc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | exec "cloc ./lib"
5 |
--------------------------------------------------------------------------------
/bin/rbs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | exec "bundle exec rbs-inline --output lib #{ARGV.join(" ")}".strip
5 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/bin/toc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | exec "bundle exec tocer upsert --root=. #{ARGV.join(" ")}".strip
5 |
--------------------------------------------------------------------------------
/fmt.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "lib/fmt/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "fmt"
7 | s.version = Fmt::VERSION
8 | s.authors = ["Nate Hopkins (hopsoft)"]
9 | s.email = ["natehop@gmail.com"]
10 |
11 | s.summary = "CLI Templating System and String Formatter"
12 | s.description = s.summary
13 | s.homepage = "https://github.com/hopsoft/fmt"
14 | s.license = "MIT"
15 | s.required_ruby_version = ">= 3.0.0"
16 |
17 | s.metadata["homepage_uri"] = s.homepage
18 | s.metadata["source_code_uri"] = s.homepage
19 |
20 | s.files = Dir["{lib,sig}/**/*", "MIT-LICENSE", "README.md"]
21 | s.require_paths = ["lib"]
22 |
23 | s.add_dependency "ast"
24 |
25 | s.add_development_dependency "activesupport", "7.1.4" # pin due to -> warning: circular require considered harmful
26 | s.add_development_dependency "amazing_print"
27 | s.add_development_dependency "fiddle"
28 | s.add_development_dependency "minitest"
29 | s.add_development_dependency "minitest-cc"
30 | s.add_development_dependency "minitest-reporters"
31 | s.add_development_dependency "ostruct"
32 | s.add_development_dependency "pry-byebug"
33 | s.add_development_dependency "pry-doc"
34 | s.add_development_dependency "rainbow"
35 | s.add_development_dependency "rake"
36 | s.add_development_dependency "rbs-inline"
37 | s.add_development_dependency "tocer"
38 | s.add_development_dependency "yard"
39 | end
40 |
--------------------------------------------------------------------------------
/lib/fmt.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 | #
5 | require_relative "fmt/boot"
6 |
7 | # Extends native Ruby String format specifications
8 | # @see https://ruby-doc.org/3.3.4/format_specifications_rdoc.html
9 | module Fmt
10 | LOCK = Monitor.new # : Monitor
11 | private_constant :LOCK
12 |
13 | # Standard error class for Fmt
14 | class Error < StandardError; end
15 |
16 | # Error for formatting failures
17 | class FormatError < Error; end
18 |
19 | class << self
20 | # Global registry for storing and retrieving String formatters i.e. Procs
21 | def registry
22 | @registry ||= LOCK.synchronize do
23 | NativeRegistry.new.merge! RainbowRegistry.new
24 | end
25 | end
26 |
27 | # Adds a keypair to the registry
28 | # @rbs key: Array[Class | Module, Symbol] -- key to use
29 | # @rbs overwrite: bool -- overwrite the existing keypair (default: false)
30 | # @rbs block: Proc -- Proc to add (optional, if proc is provided)
31 | # @rbs return: Proc
32 | def register(...)
33 | registry.add(...)
34 | end
35 |
36 | # Deletes a keypair from the registry
37 | # @rbs key: Array[Class | Module, Symbol] -- key to delete
38 | # @rbs return: Proc?
39 | def unregister(...)
40 | registry.delete(...)
41 | end
42 |
43 | # Executes a block with registry overrides
44 | #
45 | # @note Overrides will temporarily be added to the registry
46 | # and will overwrite existing entries for the duration of the block
47 | # Non overriden entries remain unchanged
48 | #
49 | # @rbs overrides: Hash[Array[Class | Module, Symbol], Proc] -- overrides to apply
50 | # @rbs block: Proc -- block to execute with overrides
51 | # @rbs return: void
52 | def with_overrides(...)
53 | registry.with_overrides(...)
54 | end
55 | end
56 | end
57 |
58 | # Top level helper for formatting and rendering a source string
59 | # @rbs source: String -- string to format
60 | # @rbs args: Array[Object] -- positional arguments (user provided)
61 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
62 | # @rbs return: String -- rendered template
63 | def Fmt(source, *args, **kwargs)
64 | ast = Fmt::TemplateParser.new(source).parse
65 | template = Fmt::Template.new(ast)
66 | renderer = Fmt::Renderer.new(template)
67 | renderer.render(*args, **kwargs)
68 | end
69 |
--------------------------------------------------------------------------------
/lib/fmt/boot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | # Standard libraries
6 | require "date"
7 | require "forwardable"
8 | require "monitor"
9 | require "ripper"
10 | require "securerandom"
11 | require "set"
12 | require "singleton"
13 | require "strscan"
14 |
15 | # 3rd party libraries
16 | require "ast"
17 |
18 | # Foundational files (globals)
19 | require_relative "sigils"
20 | require_relative "lru_cache"
21 | require_relative "mixins/matchable"
22 | require_relative "node"
23 | require_relative "renderer"
24 | require_relative "token"
25 | require_relative "tokenizer"
26 | require_relative "version"
27 |
28 | # Refinements
29 | require_relative "refinements/kernel_refinement"
30 |
31 | # Registries -- store of Procs that can be used with Fmt
32 | require_relative "registries/registry" # <- base class
33 | require_relative "registries/native_registry"
34 | require_relative "registries/rainbow_registry"
35 |
36 | # Parsers -- String | Object parsers that generate ASTs
37 | require_relative "parsers/parser" # <- base class
38 | require_relative "parsers/arguments_parser"
39 | require_relative "parsers/macro_parser"
40 | require_relative "parsers/embed_parser"
41 | require_relative "parsers/pipeline_parser"
42 | require_relative "parsers/template_parser"
43 |
44 | # Models -- data structures build from ASTs
45 | require_relative "models/model" # <- base class
46 | require_relative "models/arguments"
47 | require_relative "models/embed"
48 | require_relative "models/macro"
49 | require_relative "models/pipeline"
50 | require_relative "models/template"
51 |
--------------------------------------------------------------------------------
/lib/fmt/lru_cache.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # A threadsafe fixed-size LRU in-memory cache
7 | # Grows to capacity then evicts the least used entries
8 | #
9 | # @example
10 | # cache = Fmt::Cache.new
11 | #
12 | # cache.put :key, "value"
13 | # cache.get :key
14 | # cache.delete :key
15 | # cache.fetch :key, "default"
16 | # cache.fetch(:key) { "default" }
17 | #
18 | # @example Capacity
19 | # Fmt::Cache.capacity = 10_000
20 | class LRUCache
21 | include MonitorMixin
22 |
23 | DEFAULT_CAPACITY = 5_000 # : Integer -- default capacity
24 |
25 | # Constructor
26 | # @rbs capacity: Integer -- max capacity (negative values are uncapped, default: 5_000)
27 | # @rbs return: Fmt::Cache
28 | def initialize(capacity: DEFAULT_CAPACITY)
29 | super()
30 | @capacity = capacity
31 | @store = {}
32 | end
33 |
34 | # The cache max capacity (number of entries)
35 | # @rbs return: Integer
36 | def capacity
37 | synchronize { @capacity }
38 | end
39 |
40 | # Set the max capacity (number of entries)
41 | # @rbs capacity: Integer -- new max capacity
42 | # @rbs return: Integer -- new max capacity
43 | def capacity=(capacity)
44 | synchronize { @capacity = capacity.to_i }
45 | end
46 |
47 | # Indicates if the cache is capped
48 | # @rbs return: bool
49 | def capped?
50 | synchronize { capacity >= 0 }
51 | end
52 |
53 | # Clears the cache
54 | # @rbs return: void
55 | def clear
56 | synchronize { store.clear }
57 | end
58 |
59 | # Deletes the entry for the specified key
60 | # @rbs key: Object -- key to delete
61 | # @rbs return: Object? -- the deleted value
62 | def delete(key)
63 | synchronize { store.delete key }
64 | end
65 |
66 | # Fetches the value for the specified key
67 | # Writes the default value if the key is not found
68 | # @rbs key: Object -- key to fetch
69 | # @rbs default: Object -- default value to write
70 | # @rbs block: Proc -- block to call to get the default value
71 | # @rbs return: Object -- value
72 | def fetch(key, default = nil, &block)
73 | return get(key) if key?(key)
74 | default ||= block&.call
75 | synchronize { put key, default }
76 | end
77 |
78 | # Fetches a value from the cache without synchronization (not thread safe)
79 | # @rbs key: Object -- key to fetch
80 | # @rbs default: Object -- default value to write
81 | # @rbs block: Proc -- block to call to get the default value
82 | # @rbs return: Object -- value
83 | def fetch_unsafe(key, default = nil, &block)
84 | return store[key] if store.key?(key)
85 | store[key] = (default || block&.call)
86 | end
87 |
88 | # Indicates if the cache is full
89 | # @rbs return: bool
90 | def full?
91 | synchronize { capped? && store.size > capacity }
92 | end
93 |
94 | # Retrieves the value for the specified key
95 | # @rbs key: Object -- key to retrieve
96 | def get(key)
97 | synchronize do
98 | reposition(key) if key?(key)
99 | store[key]
100 | end
101 | end
102 |
103 | # Cache keys
104 | # @rbs return: Array[Object]
105 | def keys
106 | synchronize { store.keys }
107 | end
108 |
109 | # Indicates if the cache contains the specified key
110 | # @rbs key: Object -- key to check
111 | # @rbs return: bool
112 | def key?(key)
113 | synchronize { store.key? key }
114 | end
115 |
116 | # Stores the value for the specified key
117 | # @rbs key: Object -- key to store
118 | # @rbs value: Object -- value to store
119 | # @rbs return: Object -- value
120 | def put(key, value)
121 | synchronize do
122 | delete key if capped? # keep keey fresh if capped
123 | store[key] = value
124 | store.shift if full? # resize the cache if necessary
125 | value
126 | end
127 | end
128 |
129 | # Resets the cache capacity to the default
130 | # @rbs return: Integer -- capacity
131 | def reset_capacity
132 | synchronize { @capacity = DEFAULT_CAPACITY }
133 | end
134 |
135 | # The current size of the cache (number of entries)
136 | # @rbs return: Integer
137 | def size
138 | synchronize { store.size }
139 | end
140 |
141 | # Returns a Hash with only the given keys
142 | # @rbs keys: Array[Object] -- keys to include
143 | # @rbs return: Hash[Object, Object]
144 | def slice(*keys)
145 | synchronize { store.slice(*keys) }
146 | end
147 |
148 | # Hash representation of the cache
149 | # @rbs return: Hash[Object, Proc]
150 | def to_h
151 | synchronize { store.dup }
152 | end
153 |
154 | # Cache values
155 | # @rbs return: Array[Object]
156 | def values
157 | synchronize { store.values }
158 | end
159 |
160 | # Executes a block with a synchronized mutex
161 | # @rbs block: Proc -- block to execute
162 | def lock(&block)
163 | synchronize(&block)
164 | end
165 |
166 | alias_method :[], :get # : Object -- alias for get
167 | alias_method :[]=, :put # : Object -- alias for put
168 |
169 | private
170 |
171 | attr_reader :store # : Hash[Object, Object]
172 |
173 | # Moves the key to the end keeping it fresh
174 | # @rbs key: Object -- key to reposition
175 | # @rbs return: Object -- value
176 | def reposition(key)
177 | value = store.delete(key)
178 | store[key] = value
179 | end
180 | end
181 | end
182 |
--------------------------------------------------------------------------------
/lib/fmt/mixins/matchable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | module Matchable
7 | # Hash representation of the Object (required for pattern matching)
8 | # @rbs return: Hash[Symbol, Object]
9 | def to_h
10 | raise Error, "to_h must be implemented by including class"
11 | end
12 |
13 | # Returns a Hash representation of the object limited to the given keys
14 | # @rbs keys: Array[Symbol] -- keys to include
15 | # @rbs return: Hash[Symbol, Object]
16 | def deconstruct_keys(keys = [])
17 | to_h.select { _1 in keys }
18 | end
19 |
20 | # Returns an Array representation of the object
21 | # @rbs return: Array[Object]
22 | def deconstruct
23 | to_h.values
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/fmt/models/arguments.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Represents arguments for a method call
7 | #
8 | # Arguments are comprised of:
9 | # 1. args: Array[Object]
10 | # 2. kwargs: Hash[Symbol, Object]
11 | #
12 | class Arguments < Model
13 | # Constructor
14 | # @rbs ast: Node
15 | def initialize(ast)
16 | @args = []
17 | @kwargs = {}
18 | super
19 | end
20 |
21 | attr_reader :args # : Array[Object] -- positional arguments
22 | attr_reader :kwargs # : Hash[Symbol, Object] -- keyword arguments
23 |
24 | # Hash representation of the model (required for pattern matching)
25 | # @rbs return: Hash[Symbol, Object]
26 | def to_h
27 | super.merge(
28 | args: args,
29 | kwargs: kwargs
30 | )
31 | end
32 |
33 | # ..........................................................................
34 | # @!group AST Processors
35 | # ..........................................................................
36 |
37 | # Processes an arguments AST node
38 | # @rbs node: Node
39 | # @rbs return: void
40 | def on_arguments(node)
41 | process_all node.children
42 | end
43 |
44 | # Processes a tokens AST node
45 | # @rbs node: Node
46 | # @rbs return: void
47 | def on_tokens(node)
48 | process_all node.children
49 | end
50 |
51 | # Processes a keyword AST node
52 | # @rbs node: Node
53 | # @rbs return: nil | true | false | Object
54 | def on_kw(node)
55 | case node.children.first
56 | in "nil" then assign(nil)
57 | in "true" then assign(true)
58 | in "false" then assign(false)
59 | end
60 | end
61 |
62 | # Processes a string AST node
63 | # @rbs node: Node
64 | # @rbs return: String
65 | def on_tstring_content(node)
66 | assign node.children.first
67 | end
68 |
69 | # Processes a symbol AST Node
70 | # @rbs node: Node
71 | # @rbs return: Symbol
72 | def on_symbol(node)
73 | assign node.children.first.to_sym
74 | end
75 |
76 | # Processes a symbol start AST Node
77 | # @rbs node: Node
78 | # @rbs return: void
79 | def on_symbeg(node)
80 | @next_ident_is_symbol = true
81 | end
82 |
83 | # Processes an identifier AST Node
84 | # @rbs node: Node
85 | # @rbs return: Symbol?
86 | def on_ident(node)
87 | assign node.children.first.to_sym if @next_ident_is_symbol
88 | ensure
89 | @next_ident_is_symbol = false
90 | end
91 |
92 | # Processes an integer AST node
93 | # @rbs node: Node
94 | # @rbs return: Integer
95 | def on_int(node)
96 | assign node.children.first.to_i
97 | end
98 |
99 | # Processes a float AST node
100 | # @rbs node: Node
101 | # @rbs return: Float
102 | def on_float(node)
103 | assign node.children.first.to_f
104 | end
105 |
106 | # Processes a rational AST node
107 | # @rbs node: Node
108 | # @rbs return: Rational
109 | def on_rational(node)
110 | assign Rational(node.children.first)
111 | end
112 |
113 | # Processes an imaginary (complex) AST node
114 | # @rbs node: Node
115 | # @rbs return: Complex
116 | def on_imaginary(node)
117 | assign Complex(0, node.children.first.to_f)
118 | end
119 |
120 | # ..........................................................................
121 | # @!group Composite Data Types (Arrays, Hashes, Sets)
122 | # ..........................................................................
123 |
124 | # Processes a left bracket AST node
125 | # @rbs node: Node
126 | # @rbs return: Array
127 | def on_lbracket(node)
128 | assign([])
129 | end
130 |
131 | # Processes a left brace AST node
132 | # @rbs node: Node
133 | # @rbs return: Hash
134 | def on_lbrace(node)
135 | assign({})
136 | end
137 |
138 | # Process a label (hash key) AST node
139 | # @rbs node: Node
140 | # @rbs return: void
141 | def on_label(node)
142 | label = node.children.first
143 | label = label.chop.to_sym if label.end_with?(":")
144 | assign nil, label: label # assign placeholder
145 | end
146 |
147 | private
148 |
149 | # Assigns a value to the receiver
150 | # @rbs value: Object -- value to assign
151 | # @rbs label: Symbol? -- label to use (if applicable)
152 | # @rbs return: Object
153 | def assign(value, label: nil)
154 | receiver(label: label).tap do |rec|
155 | case rec
156 | in Array then rec << value
157 | in Hash then rec[label || rec.keys.last] = value
158 | end
159 | end
160 | end
161 |
162 | # Receiver that the processed value will be assigned to
163 | # @rbs label: Symbol? -- label to use (if applicable)
164 | # @rbs return: Array | Hash
165 | def receiver(label: nil)
166 | obj = find_receiver(kwargs) if kwargs.any?
167 | obj ||= find_receiver(args) || args
168 |
169 | case [obj, label]
170 | in [*, Symbol] then kwargs # <- 1) Array with label
171 | else obj # <------------------- 2) Composite without label
172 | end
173 | end
174 |
175 | # Finds the receiver that the processed value will be assigned to
176 | # @rbs obj: Object
177 | # @rbs return: Array? | Hash?
178 | def find_receiver(obj)
179 | case obj
180 | in [] | {} then obj # <------------------------------------------ 1) empty array/hash
181 | in [*, [*] | {**}] => array then find_receiver(array.last) # <--- 2) array with array/hash last entry
182 | in {**} => hash then find_receiver(hash.values.last) || hash # <- 3) hash with values
183 | else nil
184 | end
185 | end
186 |
187 | # Indicates if the value is a composite type (Array or Hash)
188 | # @rbs value: Object -- value to check
189 | # @rbs return: bool
190 | def composite?(value)
191 | value.is_a?(Array) || value.is_a?(Hash)
192 | end
193 | end
194 | end
195 |
--------------------------------------------------------------------------------
/lib/fmt/models/embed.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | class Embed < Model
7 | attr_reader :key # : Symbol -- key for embed
8 | attr_reader :placeholder # : String -- placeholder for embed
9 | attr_reader :template # : Template
10 |
11 | # Hash representation of the model (required for pattern matching)
12 | # @rbs return: Hash[Symbol, Object]
13 | def to_h
14 | super.merge placeholder: placeholder, template: template&.to_h
15 | end
16 |
17 | # ..........................................................................
18 | # @!group AST Processors
19 | # ..........................................................................
20 |
21 | # Processes an embed AST node
22 | # @rbs node: Node
23 | # @rbs return: void
24 | def on_embed(node)
25 | process_all node.children
26 | end
27 |
28 | # Processes a key AST node
29 | # @rbs node: Node
30 | # @rbs return: void
31 | def on_key(node)
32 | @key = node.children.first
33 | end
34 |
35 | # Processes a placeholder AST node
36 | # @rbs node: Node
37 | # @rbs return: void
38 | def on_placeholder(node)
39 | @placeholder = node.children.first
40 | end
41 |
42 | # Processes a template AST node
43 | # @rbs node: Node
44 | def on_template(node)
45 | @template = Template.new(node)
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/fmt/models/macro.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Represents an uninvoked method call
7 | #
8 | # A Macro is comprised of:
9 | # 1. name: Symbol
10 | # 2. arguments: Arguments
11 | #
12 | class Macro < Model
13 | attr_reader :name # : Symbol -- method name
14 | attr_reader :arguments # : Arguments
15 |
16 | # Constructor
17 | # @rbs ast: Node
18 | def initialize(ast)
19 | @name = nil
20 | @arguments = Arguments.new(Node.new(:arguments))
21 | super
22 | end
23 |
24 | # Hash representation of the model (required for pattern matching)
25 | # @rbs return: Hash[Symbol, Object]
26 | def to_h
27 | super.merge(
28 | name: name,
29 | arguments: arguments&.to_h
30 | )
31 | end
32 |
33 | # ..........................................................................
34 | # @!group AST Processors
35 | # ..........................................................................
36 |
37 | # Processes a macro AST node
38 | # @rbs node: Node
39 | # @rbs return: void
40 | def on_macro(node)
41 | process_all node.children
42 | end
43 |
44 | # Processes a procedure AST node
45 | # @rbs node: Node
46 | # @rbs return: void
47 | def on_name(node)
48 | @name = node.find(Symbol)
49 | end
50 |
51 | # Processes an arguments AST node
52 | # @rbs node: Node
53 | # @rbs return: void
54 | def on_arguments(node)
55 | @arguments = Arguments.new(node)
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/fmt/models/model.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Superclass for all models
7 | # @note Models are constructed from AST nodes
8 | class Model
9 | # @see http://whitequark.github.io/ast/AST/Processor/Mixin.html
10 | include AST::Processor::Mixin
11 | include Matchable
12 |
13 | # Constructor
14 | # @rbs ast: Node
15 | def initialize(ast)
16 | @ast = ast
17 | @urtext = ast.urtext
18 | @source = ast.source
19 | process ast
20 | end
21 |
22 | attr_reader :ast # : Node
23 | attr_reader :urtext # : String -- original source code
24 | attr_reader :source # : String -- parsed source code
25 |
26 | alias_method :to_s, :source # : String -- alias for source
27 |
28 | # Model inspection
29 | # @rbs return: String
30 | def inspect
31 | "#<#{self.class.name} #{inspect_properties}>"
32 | end
33 |
34 | # Indicates if a given AST node is the same AST used to construct the model
35 | # @rbs node: Node
36 | # @rbs return: bool
37 | def self?(node)
38 | node == ast
39 | end
40 |
41 | # Hash representation of the model (required for pattern matching)
42 | # @note Subclasses should override this method and call: super.merge(**)
43 | # @rbs return: Hash[Symbol, Object]
44 | def to_h
45 | {}
46 | end
47 |
48 | private
49 |
50 | # Hash of instance variables for inspection
51 | # @rbs return: Hash[String, Object]
52 | def inspectable_properties
53 | instance_variables.each_with_object({}) do |name, memo|
54 | value = instance_variable_get(name)
55 | next if value in Node
56 | memo[name[1..]] = value
57 | end
58 | end
59 |
60 | # String of inspectable properties for inspection
61 | # @rbs return: String
62 | def inspect_properties
63 | inspectable_properties.map { "#{_1}=#{_2.inspect}" }.join " "
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/fmt/models/pipeline.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Represents a series of Macros
7 | #
8 | # A Pipeline is comprised of:
9 | # 1. macros: Array[Macro]
10 | #
11 | # @note Pipelines are processed in sequence (left to right)
12 | #
13 | class Pipeline < Model
14 | # Constructor
15 | # @rbs ast: Node
16 | def initialize(ast)
17 | @macros = []
18 | super
19 | end
20 |
21 | attr_reader :macros # : Array[Node]
22 |
23 | # Hash representation of the model (required for pattern matching)
24 | # @rbs return: Hash[Symbol, Object]
25 | def to_h
26 | super.merge macros: macros.map(&:to_h)
27 | end
28 |
29 | # ..........................................................................
30 | # @!group AST Processors
31 | # ..........................................................................
32 |
33 | # Processes a pipeline AST node
34 | # @rbs node: Node
35 | # @rbs return: void
36 | def on_pipeline(node)
37 | process_all node.children
38 | end
39 |
40 | # Processes a macro AST node
41 | # @rbs node: Node
42 | # @rbs return: void
43 | def on_macro(node)
44 | @macros << Macro.new(node)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/fmt/models/template.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Represents a formattable string
7 | #
8 | # A Template is comprised of:
9 | # 1. embeds: Array[Template] -- embedded templates
10 | # 2. pipelines :: Array[Pipeline] -- sets of Macros
11 | #
12 | # @note Embeds are processed from inner to outer
13 | #
14 | class Template < Model
15 | # Constructor
16 | # @rbs ast: Node
17 | def initialize(ast)
18 | @embeds = []
19 | @pipelines = []
20 | super
21 | end
22 |
23 | attr_reader :embeds # : Array[Template]
24 | attr_reader :pipelines # : Array[Pipeline]
25 |
26 | # @rbs return: Hash[Symbol, Object]
27 | def to_h
28 | super.merge embeds: embeds.map(&:to_h), pipelines: pipelines.map(&:to_h)
29 | end
30 |
31 | # ..........................................................................
32 | # @!group AST Processors
33 | # ..........................................................................
34 |
35 | def on_template(node)
36 | process_all node.children
37 | end
38 |
39 | def on_embeds(node)
40 | process_all node.children
41 | end
42 |
43 | def on_embed(node)
44 | embeds << Embed.new(node)
45 | end
46 |
47 | def on_pipelines(node)
48 | process_all node.children
49 | end
50 |
51 | def on_pipeline(node)
52 | pipelines << Pipeline.new(node)
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/fmt/node.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Extends behavior of AST::Node
7 | class Node < AST::Node
8 | extend Forwardable
9 |
10 | class << self
11 | # Finds all Node child nodes
12 | # @rbs node: Node -- node to search
13 | # @rbs return: Array[Node]
14 | def node_children(node)
15 | list = []
16 | node.children.each do |child|
17 | list << child if child.is_a?(Node)
18 | end
19 | list
20 | end
21 |
22 | # Recursively finds all Nodes in the tree
23 | # @rbs node: Node -- node to search
24 | # @rbs return: Array[Node]
25 | def node_descendants(node)
26 | list = []
27 | node.children.each do |child|
28 | list << child if child.is_a?(Node)
29 | list.concat node_children(child)
30 | end
31 | list
32 | end
33 | end
34 |
35 | # Constructor
36 | # @rbs type: Symbol
37 | # @rbs children: Array[Node]
38 | # @rbs properties: Hash[Symbol, Object]
39 | def initialize(type, children = [], properties = {urtext: "", source: ""})
40 | @properties = properties
41 | define_properties properties
42 | super
43 | end
44 |
45 | attr_reader :properties # : Hash[Symbol, Object]
46 |
47 | # Returns the child at the specified index
48 | # @rbs index: Integer -- index of child node
49 | # @rbs return: Node? | Object?
50 | def_delegator :children, :[]
51 |
52 | # Indicates if no children exist
53 | # @rbs return: bool
54 | def_delegator :children, :empty?
55 |
56 | # Returns the number of children
57 | # @rbs return: Integer
58 | def_delegator :children, :size
59 |
60 | # Recursively searches the tree for a descendant node
61 | # @rbs types: Array[Object] -- node types to find
62 | # @rbs return: Node?
63 | def dig(*types)
64 | node = find(types.shift) if types.any?
65 | node = node.find(types.shift) while node && types.any?
66 | node
67 | end
68 |
69 | # Finds the first child node of the specified type
70 | # @rbs type: Object -- node type to find
71 | # @rbs return: Node?
72 | def find(type)
73 | case type
74 | in Symbol then children.find { _1 in [^type, *] }
75 | in Class then children.find { _1 in ^type }
76 | end
77 | end
78 |
79 | # Flattens Node descendants into a one dimensional array
80 | # @rbs return: Array[Node]
81 | def flatten
82 | node_descendants.prepend self
83 | end
84 |
85 | # Finds all child nodes of the specified type
86 | # @rbs type: Object -- node type to select
87 | # @rbs return: Node?
88 | def select(type)
89 | [].concat case type
90 | in Symbol then children.select { _1 in [^type, *] }
91 | in Class then children.select { _1 in ^type }
92 | else []
93 | end
94 | end
95 |
96 | # String representation of the node (AST)
97 | # @rbs squish: bool -- remove extra whitespace
98 | # @rbs return: String
99 | def to_s(squish: false)
100 | value = super()
101 | return value unless squish
102 | value.gsub(/\s{2,}/, " ")
103 | end
104 |
105 | private
106 |
107 | # Finds all Node child nodes
108 | # @rbs return: Array[Node]
109 | def node_children
110 | self.class.node_children self
111 | end
112 |
113 | # Recursively finds all Node nodes in the tree
114 | # @rbs return: Array[Node]
115 | def node_descendants
116 | self.class.node_descendants self
117 | end
118 |
119 | # Defines accessor methods for properties on the receiver
120 | # @rbs properties: Hash[Symbol, Object] -- exposed as instance methods
121 | def define_properties(properties)
122 | properties.each do |key, val|
123 | next if singleton_class.public_instance_methods(false).include?(key)
124 | singleton_class.define_method(key) { val }
125 | end
126 | end
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/lib/fmt/parsers/arguments_parser.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Parses arguments from a string and builds an AST (Abstract Syntax Tree)
7 | class ArgumentsParser < Parser
8 | # Constructor
9 | # @rbs tokens: Array[Token] -- wrapped ripper tokens
10 | def initialize(tokens = [])
11 | @tokens = tokens
12 | end
13 |
14 | attr_reader :tokens # : Array[Token] -- wrapped ripper tokens
15 |
16 | # Parses the urtext (original source code)
17 | # @rbs return: Node -- AST (Abstract Syntax Tree)
18 | def parse
19 | cache(tokens.to_s) { super }
20 | end
21 |
22 | protected
23 |
24 | # Extracts components for building the AST (Abstract Syntax Tree)
25 | # @rbs return: Hash[Symbol, Object] -- extracted components
26 | def extract
27 | {tokens: tokens}
28 | end
29 |
30 | # Transforms extracted components into an AST (Abstract Syntax Tree)
31 | # @rbs tokens: Array[Token] -- extracted tokens
32 | # @rbs return: Node -- AST (Abstract Syntax Tree)
33 | def transform(tokens:)
34 | return Node.new(:arguments) if tokens.none?
35 |
36 | source = tokens.map(&:value).join
37 | tokens = tokens.map { |t| Node.new(t.type, [t.value], urtext: t.value, source: t.value) }
38 | tokens = Node.new(:tokens, tokens, urtext: source, source: source)
39 |
40 | Node.new :arguments, [tokens], urtext: source, source: source
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/fmt/parsers/embed_parser.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Parses embeds from a string and builds an AST (Abstract Syntax Tree)
7 | class EmbedParser < Parser
8 | # Constructor
9 | # @rbs urtext: String -- original source code
10 | # @rbs key: Symbol -- key for embed
11 | # @rbs placeholder: String -- placeholder for embed
12 | def initialize(urtext = "", key:, placeholder:)
13 | @urtext = urtext.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
14 | @key = key
15 | @placeholder = placeholder
16 | end
17 |
18 | attr_reader :urtext # : String -- original source code
19 | attr_reader :key # : Symbol -- key for embed
20 | attr_reader :placeholder # : String -- placeholder for embed
21 |
22 | # Parses the urtext (original source code)
23 | # @rbs return: Node -- AST (Abstract Syntax Tree)
24 | def parse
25 | cache(urtext) { super }
26 | end
27 |
28 | protected
29 |
30 | # Extracts components for building the AST (Abstract Syntax Tree)
31 | # @rbs return: Hash[Symbol, Object] -- extracted components
32 | def extract
33 | source = urtext.delete_prefix(Sigils::EMBED_PREFIX).delete_suffix(Sigils::EMBED_SUFFIX)
34 | {source: source}
35 | end
36 |
37 | # Transforms extracted components into an AST (Abstract Syntax Tree)
38 | # @rbs return: Node -- AST (Abstract Syntax Tree)
39 | def transform(source:)
40 | key = Node.new(:key, [self.key])
41 | placeholder = Node.new(:placeholder, [self.placeholder])
42 | template = TemplateParser.new(source).parse
43 | children = [key, placeholder, template].reject(&:empty?)
44 | Node.new(:embed, children, urtext: urtext, source: source)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/fmt/parsers/macro_parser.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Parses a macro from a string and builds an AST (Abstract Syntax Tree)
7 | class MacroParser < Parser
8 | # Constructor
9 | # @rbs urtext: String -- original source code
10 | def initialize(urtext = "")
11 | @urtext = urtext.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
12 | end
13 |
14 | attr_reader :urtext # : String -- original source code
15 |
16 | # Parses the urtext (original source code)
17 | # @rbs return: Node -- AST (Abstract Syntax Tree)
18 | def parse
19 | cache(urtext) { super }
20 | end
21 |
22 | protected
23 |
24 | # Extracts components for building the AST (Abstract Syntax Tree)
25 | # @rbs return: Hash[Symbol, Object] -- extracted components
26 | def extract
27 | code = urtext.delete_prefix(Sigils::FORMAT_PREFIX)
28 | tokens = tokenize(code)
29 | method = tokens.find(&:method_name?)&.value&.to_sym
30 |
31 | arguments_tokens = case arguments?(tokens)
32 | in false then []
33 | else
34 | arguments_start = tokens.index(tokens.find(&:arguments_start?)).to_i
35 | arguments_finish = tokens.index(tokens.find(&:arguments_finish?)).to_i
36 | tokens[arguments_start..arguments_finish]
37 | end
38 |
39 | {method: method, arguments_tokens: arguments_tokens}
40 | end
41 |
42 | # Transforms extracted components into an AST (Abstract Syntax Tree)
43 | # @rbs method: Symbol?
44 | # @rbs arguments_tokens: Array[Token] -- arguments tokens
45 | # @rbs return: Node -- AST (Abstract Syntax Tree)
46 | def transform(method:, arguments_tokens:)
47 | method = Node.new(:name, [method], urtext: urtext, source: method.to_s)
48 | arguments = ArgumentsParser.new(arguments_tokens).parse
49 | source = "#{method.source}#{arguments.source}"
50 | children = [method, arguments].reject(&:empty?)
51 |
52 | Node.new :macro, children.reject(&:empty?), urtext: urtext, source: source
53 | end
54 |
55 | private
56 |
57 | # Tokenizes source code
58 | # @rbs code: String -- source code to tokenize
59 | # @rbs return: Array[Token] -- wrapped ripper tokens
60 | def tokenize(code)
61 | tokens = Tokenizer.new(code).tokenize
62 | macro = []
63 |
64 | tokens.each do |token|
65 | break if token.whitespace? && macro_finished?(macro)
66 | macro << token
67 | end
68 |
69 | macro
70 | end
71 |
72 | # Indicates if there is a set of arguments in the tokens
73 | # @rbs tokens: Array[Token] -- tokens to check
74 | # @rbs return: bool
75 | def arguments?(tokens)
76 | arguments_started?(tokens) && arguments_finished?(tokens)
77 | end
78 |
79 | # Indicates if arguments have started
80 | # @rbs tokens: Array[Token] -- tokens to check
81 | # @rbs return: bool
82 | def arguments_started?(tokens)
83 | tokens.any? { _1.arguments_start? }
84 | end
85 |
86 | # Indicates if arguments have finished
87 | # @note Call this after a whitespace has been detected
88 | # @rbs tokens: Array[Token] -- tokens to check
89 | # @rbs return: bool
90 | def arguments_finished?(tokens)
91 | tokens.any? { _1.arguments_finish? }
92 | end
93 |
94 | # Indicates if a macro token array is complete or finished
95 | # @note Call this after a whitespace has been detected
96 | # @rbs tokens: Array[Token] -- tokens to check
97 | # @rbs return: bool
98 | def finished?(tokens)
99 | return false unless tokens.any? { _1.method_name? }
100 | return false if arguments_started?(tokens) && !arguments_finished?(tokens)
101 | true
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/lib/fmt/parsers/parser.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Responsible for parsing various inputs and returning an AST (Abstract Syntax Tree)
7 | #
8 | # Mechanics are similar to an ETL pipeline (Extract, Transform, Load), however,
9 | # parsers only handle extracting and transforming.
10 | #
11 | # Loading is handled by AST processors (Models)
12 | # @see lib/fmt/models/
13 | class Parser
14 | Cache = Fmt::LRUCache.new # : Fmt::LRUCache -- local in-memory cache
15 |
16 | # Escapes a string for use in a regular expression
17 | # @rbs value: String -- string to escape
18 | # @rbs return: String -- escaped string
19 | def self.esc(value) = Regexp.escape(value.to_s)
20 |
21 | # Parses input passed to the constructor and returns an AST (Abstract Syntax Tree)
22 | #
23 | # 1. Extract components
24 | # 2. Transform to AST
25 | #
26 | # @note Subclasses must implement the extract and transform methods
27 | #
28 | # @rbs return: Node -- AST (Abstract Syntax Tree)
29 | def parse
30 | extract.then { transform(**_1) }
31 | end
32 |
33 | protected
34 |
35 | # Extracts components for building the AST (Abstract Syntax Tree)
36 | # @rbs return: Hash[Symbol, Object] -- extracted components
37 | def extract
38 | raise Error, "extract must be implemented by subclass"
39 | end
40 |
41 | # Transforms extracted components into an AST (Abstract Syntax Tree)
42 | # @rbs kwargs: Hash[Symbol, Object] -- extracted components
43 | # @rbs return: Node -- AST (Abstract Syntax Tree)
44 | def transform(**kwargs)
45 | raise Error, "transform must be implemented by subclass"
46 | end
47 |
48 | # Cache helper that fetches a value from the cache
49 | # @rbs key: String -- cache key
50 | # @rbs block: Proc -- block to execute if the value is not found in the cache
51 | # @rbs return: Object
52 | def cache(key, &block)
53 | Cache.fetch_unsafe("#{self.class.name}/#{key}") { yield }
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/fmt/parsers/pipeline_parser.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Parses a pipeline from a string and builds an AST (Abstract Syntax Tree)
7 | class PipelineParser < Parser
8 | # Constructor
9 | # @rbs urtext: String -- original source code
10 | def initialize(urtext = "")
11 | @urtext = urtext.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
12 | end
13 |
14 | attr_reader :urtext # : String -- original source code
15 |
16 | # Parses the urtext (original source code)
17 | # @rbs return: Node -- AST (Abstract Syntax Tree)
18 | def parse
19 | cache(urtext) { super }
20 | end
21 |
22 | protected
23 |
24 | # Extracts components for building the AST (Abstract Syntax Tree)
25 | # @rbs return: Hash[Symbol, Object] -- extracted components
26 | def extract
27 | macros = urtext.split(Sigils::PIPE_OPERATOR).map(&:strip).reject(&:empty?)
28 | {macros: macros}
29 | end
30 |
31 | # Transforms extracted components into an AST (Abstract Syntax Tree)
32 | # @rbs macros: Array[Array[Token]] -- extracted macro tokens
33 | # @rbs return: Node -- AST (Abstract Syntax Tree)
34 | def transform(macros:)
35 | macros = macros.map { |macro_urtext, memo| MacroParser.new(macro_urtext).parse }.reject(&:empty?)
36 | source = macros.map(&:source).join(Sigils::PIPE_OPERATOR).strip
37 |
38 | Node.new :pipeline, macros, urtext: urtext, source: source
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/fmt/parsers/template_parser.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Parses a template from a string and builds an AST (Abstract Syntax Tree)
7 | class TemplateParser < Parser
8 | EMBED_PEEK = %r{(?=#{esc Sigils::EMBED_PREFIX})}ou # : Regexp -- detects start of an embed prefix (look ahead)
9 | PIPELINE_PEEK = %r{(?=[#{Sigils::FORMAT_PREFIX}][^#{Sigils::FORMAT_PREFIX}])}ou # : Regexp -- detects start of a pipeline (look ahead)
10 | PERCENT_LITERAL = %r{[#{Sigils::FORMAT_PREFIX}]{2}}ou # : Regexp -- detects a percent literal
11 | WHITESPACE = %r{\s}ou # : Regexp -- detects whitespace
12 |
13 | # Constructor
14 | # @rbs urtext: String -- original source code
15 | def initialize(urtext = "")
16 | @urtext = urtext.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
17 | end
18 |
19 | attr_reader :urtext # : String -- original source code
20 |
21 | # Parses the urtext (original source code)
22 | # @rbs return: Node -- AST (Abstract Syntax Tree)
23 | def parse
24 | cache(urtext) { super }
25 | end
26 |
27 | protected
28 |
29 | # Extracts components for building the AST (Abstract Syntax Tree)
30 | # @note Extraction is delegated to the PipelineParser and EmbedParser in transform
31 | # @rbs return: Hash
32 | def extract
33 | source = urtext
34 |
35 | # 1) extract embeds first and update the source
36 | embeds = extract_embeds(source)
37 | embeds.each do |embed|
38 | source = source.sub(embed[:urtext], embed[:placeholder])
39 | end
40 |
41 | # 2) extract pipelines
42 | pipelines = extract_pipelines(source)
43 |
44 | {embeds: embeds, pipelines: pipelines, source: source}
45 | end
46 |
47 | # Transforms extracted components into an AST (Abstract Syntax Tree)
48 | # @rbs embeds: Array[Hash] -- extracted embeds
49 | # @rbs pipelines: Array[String] -- extracted pipelines
50 | # @rbs source: String -- parsed source code
51 | # @rbs return: Node -- AST (Abstract Syntax Tree)
52 | def transform(embeds:, pipelines:, source:)
53 | embeds = embeds.map { EmbedParser.new(_1[:urtext], **_1.slice(:key, :placeholder)).parse }
54 | embeds = Node.new(:embeds, embeds, urtext: urtext, source: urtext)
55 |
56 | pipelines = pipelines.map { PipelineParser.new(_1).parse }
57 | pipelines = Node.new(:pipelines, pipelines, urtext: urtext, source: source)
58 |
59 | children = [embeds, pipelines].reject(&:empty?)
60 |
61 | Node.new :template, children, urtext: urtext, source: source
62 | end
63 |
64 | private
65 |
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 | head = Sigils::EMBED_PREFIX[0]
73 | tail = Sigils::EMBED_SUFFIX[0]
74 | index = scanner.pos
75 | stack = 0
76 |
77 | until scanner.eos?
78 | case scanner.getch
79 | in ^head then stack += 1
80 | in ^tail then stack -= 1
81 | in nil then break
82 | else # noop
83 | end
84 |
85 | break if stack.zero?
86 | end
87 |
88 | return nil unless stack.zero?
89 |
90 | scanner.string[index...scanner.pos]
91 | end
92 |
93 | # Extracts embed metadata from the source
94 | # @rbs return: Array[Hash] -- extracted embeds
95 | def extract_embeds(source)
96 | scanner = StringScanner.new(source)
97 |
98 | # will iterate until extract_next_embed returns nil... when run
99 | generator = Enumerator.new do |yielder|
100 | while (embed = extract_next_embed(scanner))
101 | yielder << embed
102 | end
103 | end
104 |
105 | # runs the generator and returns the resulting array
106 | embeds = generator.to_a
107 |
108 | embeds.map.with_index do |embed, index|
109 | key = :"embed_#{index}"
110 | placeholder = "#{Sigils::FORMAT_PREFIX}#{Sigils::KEY_PREFIXES[-1]}#{key}#{Sigils::KEY_SUFFIXES[-1]}"
111 | {key: key, placeholder: placeholder, urtext: embed}
112 | end
113 | end
114 |
115 | # Extracts the next pipeline with the scanner
116 | # @rbs scanner: StringScanner -- scanner to extract from
117 | # @rbs return: String? -- extracted pipeline
118 | def extract_next_pipeline(scanner)
119 | return nil unless scanner.skip_until(PIPELINE_PEEK)
120 |
121 | index = scanner.charpos
122 |
123 | until scanner.eos?
124 | len = 1
125 | val = nil
126 | val = scanner.peek(len += 1) until val&.valid_encoding?
127 | if val&.match? PERCENT_LITERAL
128 | scanner.pos += len
129 | next
130 | end
131 |
132 | len = 0
133 | val = nil
134 | val = scanner.peek(len += 1) until val&.valid_encoding?
135 | case [index, scanner.charpos, val]
136 | in [i, pos, Sigils::FORMAT_PREFIX] if i == pos then scanner.getch
137 | in [i, pos, Sigils::FORMAT_PREFIX] if i != pos then break
138 | in [i, pos, WHITESPACE] if arguments_balanced?(scanner.string[i...pos]) then break
139 | else scanner.getch
140 | end
141 | end
142 |
143 | scanner.string[index...scanner.charpos]
144 | end
145 |
146 | # Extracts pipelines from the source
147 | # @rbs source: String -- source code to extract pipelines from
148 | # @rbs return: Array[String] -- extracted pipelines
149 | def extract_pipelines(source)
150 | scanner = StringScanner.new(source)
151 |
152 | generator = Enumerator.new do |yielder|
153 | while (pipeline = extract_next_pipeline(scanner))
154 | yielder << pipeline
155 | end
156 | end
157 |
158 | generator.to_a
159 | end
160 |
161 | # Indicates if arguments are balances in the given list of chars
162 | # @rbs value: String -- value to check
163 | # @rbs return: bool
164 | def arguments_balanced?(value)
165 | return true if value.nil? || value.empty?
166 | value.count(Sigils::ARGS_PREFIX) == value.count(Sigils::ARGS_SUFFIX)
167 | end
168 | end
169 | end
170 |
--------------------------------------------------------------------------------
/lib/fmt/refinements/kernel_refinement.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | module KernelRefinement
7 | refine Kernel do
8 | # Formats an object with Fmt
9 | # @rbs object [Object] -- object to format (coerced to String)
10 | # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
11 | # @rbs return [String] -- formatted text
12 | def fmt(object, *pipeline)
13 | Fmt pipeline.prepend("%s").join(Sigils::PIPE_OPERATOR), object
14 | end
15 |
16 | # Formats an object with Fmt and prints to STDOUT
17 | # @rbs object [Object] -- object to format (coerced to String)
18 | # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
19 | # @rbs return void
20 | def fmt_print(object, *pipeline)
21 | print fmt(object, *pipeline)
22 | end
23 |
24 | # Formats an object with Fmt and puts to STDOUT
25 | # @rbs object [Object] -- object to format (coerced to String)
26 | # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
27 | # @rbs return void
28 | def fmt_puts(object, *pipeline)
29 | puts fmt(object, *pipeline)
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/fmt/registries/native_registry.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Extends native Ruby String format specifications with native Ruby methods
7 | # @see https://ruby-doc.org/3.3.4/format_specifications_rdoc.html
8 | class NativeRegistry < Registry
9 | SUPPORTED_CLASSES = [
10 | Array,
11 | Date,
12 | DateTime,
13 | FalseClass,
14 | Float,
15 | Hash,
16 | Integer,
17 | NilClass,
18 | Range,
19 | Regexp,
20 | Set,
21 | StandardError,
22 | String,
23 | Struct,
24 | Symbol,
25 | Time,
26 | TrueClass
27 | ].freeze
28 |
29 | # Constructor
30 | def initialize
31 | super
32 |
33 | format = ->(*args, **kwargs) do
34 | verbose = $VERBOSE
35 | $VERBOSE = nil
36 | Kernel.sprintf(self, *args, **kwargs)
37 | ensure
38 | $VERBOSE = verbose
39 | end
40 |
41 | add([Kernel, :format], &format)
42 | add([Kernel, :sprintf], &format)
43 |
44 | SUPPORTED_CLASSES.each do |klass|
45 | supported_method_names(klass).each do |name|
46 | add([klass, name]) { |*args, **kwargs| public_send(name, *args, **kwargs) }
47 | end
48 | end
49 | rescue => error
50 | puts "#{self.class.name} - Error adding filters! #{error.inspect}"
51 | end
52 |
53 | private
54 |
55 | # Array of supported method names for a Class
56 | # @rbs klass: Class
57 | # @rbs return: Array[Symbol]
58 | def supported_method_names(klass)
59 | klass.public_instance_methods.each_with_object(Set.new) do |name, memo|
60 | next if name in Sigils::FORMAT_SPECIFIERS
61 | next if name.start_with?("_") || name.end_with?("!")
62 | memo << name
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/fmt/registries/rainbow_registry.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Extends native Ruby String format specifications with Rainbow methods
7 | # @see https://ruby-doc.org/3.3.4/format_specifications_rdoc.html
8 | # @note Rainbow macros convert the Object to a String
9 | class RainbowRegistry < Registry
10 | # Constructor
11 | def initialize
12 | super
13 |
14 | if defined? Rainbow
15 | add([Object, :rainbow]) { Rainbow self }
16 | add([Object, :bg]) { |*args, **kwargs| Rainbow(self).bg(*args, **kwargs) }
17 | add([Object, :color]) { |*args, **kwargs| Rainbow(self).color(*args, **kwargs) }
18 |
19 | methods = Rainbow::Presenter.public_instance_methods(false).select do
20 | Rainbow::Presenter.public_instance_method(_1).arity == 0
21 | end
22 |
23 | method_names = methods
24 | .map { _1.name.to_sym }
25 | .concat(Rainbow::X11ColorNames::NAMES.keys)
26 | .sort
27 |
28 | method_names.each do |name|
29 | add([Object, name]) { Rainbow(self).public_send name }
30 | end
31 | end
32 | rescue => error
33 | puts "#{self.class.name} - Error adding filters! #{error.inspect}"
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/fmt/registries/registry.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Registry for storing and retrieving String formatters i.e. Procs
7 | class Registry
8 | extend Forwardable
9 |
10 | INSTANCE_VAR = :@fmt_registry_key # : Symbol -- instance variable set on registered Procs
11 | private_constant :INSTANCE_VAR
12 |
13 | # Constructor
14 | def initialize
15 | @store = LRUCache.new(capacity: -1)
16 | end
17 |
18 | def_delegator :store, :to_h # : Hash[Symbol, Proc]
19 | def_delegator :store, :[] # : Proc -- retrieves a Proc from the registry
20 | def_delegator :store, :key? # : bool -- indicates if a key exists in the registry
21 |
22 | # Indicates if a method name is registered for any Class
23 | # @rbs method_name: Symbol -- method name to check
24 | # @rbs return: bool
25 | def any?(method_name)
26 | !!method_names[method_name]
27 | end
28 |
29 | # Indicates if a method name is unregistered
30 | # @rbs method_name: Symbol -- method name to check
31 | # @rbs return: bool
32 | def none?(method_name)
33 | !any?(method_name)
34 | end
35 |
36 | # Adds a keypair to the registry
37 | # @rbs key: Array[Class | Module, Symbol] -- key to use
38 | # @rbs overwrite: bool -- overwrite the existing keypair (default: false)
39 | # @rbs block: Proc -- Proc to add (optional, if proc is provided)
40 | # @rbs return: Proc
41 | def add(key, overwrite: false, &block)
42 | raise Error, "key must be an Array[Class | Module, Symbol]" unless key in [Class | Module, Symbol]
43 |
44 | return store[key] if store.key?(key) && !overwrite
45 |
46 | store.lock do
47 | store[key] = block
48 | block.instance_variable_set INSTANCE_VAR, key
49 | end
50 |
51 | block
52 | end
53 |
54 | # Deletes a keypair from the registry
55 | # @rbs key: Array[Class | Module, Symbol] -- key to delete
56 | # @rbs return: Proc?
57 | def delete(key)
58 | store.lock do
59 | callable = store.delete(key)
60 | callable&.remove_instance_variable INSTANCE_VAR
61 | end
62 | end
63 |
64 | # Fetches a Proc from the registry
65 | # @rbs key: Array[Class | Module, Symbol] -- key to retrieve
66 | # @rbs callable: Proc -- Proc to use if the key is not found (optional, if block is provided)
67 | # @rbs block: Proc -- block to use if the key is not found (optional, if proc is provided)
68 | # @rbs return: Proc
69 | def fetch(key, callable: nil, &block)
70 | callable ||= block
71 | store[key] || add(key, &callable)
72 | end
73 |
74 | # Retrieves the registered key for a Proc
75 | # @rbs callable: Proc -- Proc to retrieve the key for
76 | # @rbs return: Symbol?
77 | def key_for(callable)
78 | callable&.instance_variable_get INSTANCE_VAR
79 | end
80 |
81 | # Merges another registry into this one
82 | # @rbs other: Fmt::Registry -- other registry to merge
83 | # @rbs return: Fmt::Registry
84 | def merge!(other)
85 | raise Error, "other must be a registry" unless other in Registry
86 | other.to_h.each { add(_1, &_2) }
87 | self
88 | end
89 |
90 | # Executes a block with registry overrides
91 | #
92 | # @note Overrides will temporarily be added to the registry
93 | # and will overwrite existing entries for the duration of the block
94 | # Non overriden entries remain unchanged
95 | #
96 | # @rbs overrides: Hash[Array[Class | Module, Symbol], Proc] -- overrides to apply
97 | # @rbs block: Proc -- block to execute with overrides
98 | # @rbs return: void
99 | def with_overrides(overrides, &block)
100 | return yield unless overrides in Hash
101 | return yield unless overrides&.any?
102 |
103 | overrides.select! { [_1, _2] in [[Class | Module, Symbol], Proc] }
104 | originals = store.slice(*(store.keys & overrides.keys))
105 |
106 | store.lock do
107 | overrides.each { add(_1, overwrite: true, &_2) }
108 | yield
109 | end
110 | ensure
111 | store.lock do
112 | overrides&.each { delete _1 }
113 | originals&.each { add(_1, overwrite: true, &_2) }
114 | end
115 | end
116 |
117 | protected
118 |
119 | attr_reader :store # : LRUCache
120 |
121 | # Hash of registered method names
122 | # @rbs return: Hash[Symbol, TrueClass]
123 | def method_names
124 | store.keys.each_with_object({}) { _2[_1.last] = true }
125 | end
126 | end
127 | end
128 |
--------------------------------------------------------------------------------
/lib/fmt/renderer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Renders templates to a formatted string
7 | class Renderer
8 | PIPELINE_START = Regexp.new("(?=%s)" % [Sigils::FORMAT_PREFIX]).freeze # : Regexp -- detects start of first pipeline
9 |
10 | # Constructor
11 | # @rbs template: Template
12 | def initialize(template)
13 | @template = template
14 | end
15 |
16 | attr_reader :template # : Template
17 |
18 | # Renders the template to a string
19 | # @note Positional and Keyword arguments are mutually exclusive
20 | # @rbs args: Array[Object] -- positional arguments (user provided)
21 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
22 | # @rbs return: String -- rendered template
23 | def render(*args, **kwargs)
24 | raise Error, "positional and keyword arguments are mutually exclusive" if args.any? && kwargs.any?
25 |
26 | render_embeds(*args, **kwargs) do |embed, result|
27 | kwargs[embed.key] = result
28 | end
29 |
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
35 | end
36 |
37 | private
38 |
39 | # Renders all template embeds
40 | # @rbs args: Array[Object] -- positional arguments (user provided)
41 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
42 | # @rbs &block: Proc -- block executed for each embed (signature: Proc(Embed, String))
43 | def render_embeds(*args, **kwargs)
44 | template.embeds.each do |embed|
45 | yield embed, Renderer.new(embed.template).render(*args, **kwargs)
46 | end
47 | end
48 |
49 | # Renders all template pipelines
50 | # @rbs args: Array[Object] -- positional arguments (user provided)
51 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
52 | # @rbs block: Proc -- block executed for each pipeline (signature: Proc(Pipeline, String))
53 | def render_pipelines(*args, **kwargs)
54 | template.pipelines.each_with_index do |pipeline, index|
55 | yield pipeline, render_pipeline(pipeline, *args[index..], **kwargs)
56 | end
57 | end
58 |
59 | # Renders a single pipeline
60 | # @rbs pipeline: Pipeline -- pipeline to render
61 | # @rbs args: Array[Object] -- positional arguments (user provided)
62 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
63 | # @rbs return: String
64 | def render_pipeline(pipeline, *args, **kwargs)
65 | result = nil
66 |
67 | pipeline.macros.each do |macro|
68 | result = invoke_macro(result, macro, *args, **kwargs)
69 | end
70 |
71 | result
72 | end
73 |
74 | # Invokes a macro
75 | # @rbs context: Object -- self in callable (Proc)
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)
79 | # @rbs return: Object -- result
80 | def invoke_macro(context, macro, *args, **kwargs)
81 | callable = Fmt.registry[[context.class, macro.name]] || Fmt.registry[[Object, macro.name]]
82 |
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
95 | rescue => error
96 | args ||= []
97 | kwargs ||= {}
98 | raise_format_error(macro, *args, cause: error, **kwargs)
99 | end
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 |
112 | # Raises an invocation error if/when Proc invocations fail
113 | # @rbs macro: Macro -- macro that failed
114 | # @rbs args: Array[Object] -- positional arguments (user provided)
115 | # @rbs cause: Exception -- exception that caused the error
116 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
117 | # @rbs return: void
118 | def raise_format_error(macro, *args, cause:, **kwargs)
119 | raise FormatError, "Error in macro! `#{macro.urtext}` args=#{args.inspect} kwargs=#{kwargs.inspect} cause=#{cause.inspect}"
120 | end
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/lib/fmt/sigils.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Common Fmt sigils (used in String templates)
7 | class Sigils
8 | # Native Ruby format specifiers
9 | # @see https://docs.ruby-lang.org/en/master/format_specifications_rdoc.html
10 | FORMAT_PREFIX = "%" # : String -- start of a format string (i.e. a template)
11 | FORMAT_SPECIFIERS = %w[A E G X a b c d e f g i o p s u x].freeze # : Array[String] -- format specifiers
12 | FORMAT_FLAGS = [" ", "#", "+", "-", "0", ":", "::", "^", "_"].freeze # : Array[String] -- format flags
13 | FORMAT_METHOD = :sprintf # : Symbol -- format method name
14 |
15 | KEY_PREFIXES = ["<", "{"].freeze # : Array[String] -- keyed template prefix
16 | KEY_SUFFIXES = [">", "}"].freeze # : Array[String] -- keyed template suffix
17 | ARGS_PREFIX = "(" # : String -- macro arguments prefix
18 | ARGS_SUFFIX = ")" # : String -- macro arguments suffix
19 | PIPE_OPERATOR = "|>" # : String -- macro delimiter
20 | EMBED_PREFIX = "{{" # : String -- embed prefix
21 | EMBED_SUFFIX = "}}" # : String -- embed prefix
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/fmt/token.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Convenience wrapper for Ripper tokens
7 | #
8 | # @see https://rubyapi.org/3.4/o/ripper
9 | # @see doc/RIPPER.md (cheetsheet)
10 | #
11 | # @example Ripper Token
12 | # [[lineno, column], type, token, state]
13 | # [[Integer, Integer], Symbol, String, Object]
14 | #
15 | class Token
16 | include Matchable
17 |
18 | # Constructor
19 | # @rbs ripper_token: Array[[Integer, Integer], Symbol, String, Object] -- Ripper token
20 | def initialize(ripper_token)
21 | (lineno, column), type, token, state = ripper_token
22 | @ripper_token = ripper_token
23 | @lineno = lineno
24 | @column = column
25 | @type = type.to_s.delete_prefix("on_").to_sym # strip Ripper's "on_" prefix for parser semantics
26 | @token = token
27 | @state = state
28 | freeze
29 | end
30 |
31 | attr_reader :ripper_token # : Array[[Integer, Integer], Symbol, String, Object]
32 | attr_reader :lineno # : Integer
33 | attr_reader :column # : Integer
34 | attr_reader :type # : Symbol
35 | attr_reader :token # : String
36 | attr_reader :state # : Object
37 |
38 | # @note The entire data structure is considered a "token"
39 | # Alias the embedded "token" as "value" to reduce confusion
40 | alias_method :value, :token
41 |
42 | # Returns a Hash representation of the token
43 | # @rbs return: Hash[Symbol, Object]
44 | def to_h
45 | {
46 | lineno: lineno,
47 | column: column,
48 | type: type,
49 | token: token,
50 | value: token,
51 | state: state
52 | }
53 | end
54 |
55 | # --------------------------------------------------------------------------
56 | # @!group Pattern Matching Support
57 | # --------------------------------------------------------------------------
58 | alias_method :deconstruct, :ripper_token
59 |
60 | # Returns a Hash representation of the token limited to the given keys
61 | # @rbs keys: Array[Symbol] -- keys to include
62 | # @rbs return: Hash[Symbol, Object]
63 | def deconstruct_keys(keys = [])
64 | to_h.select { _1 in ^keys }
65 | end
66 |
67 | # --------------------------------------------------------------------------
68 | # @!group Helpers
69 | # --------------------------------------------------------------------------
70 |
71 | # Indicates if the token is a left paren (i.e. start of arguments)
72 | # @rbs return: bool
73 | def arguments_start?
74 | type == :lparen
75 | end
76 |
77 | # Indicates if the token is a right paren (i.e. end of arguments)
78 | # @rbs return: bool
79 | def arguments_finish?
80 | type == :rparen
81 | end
82 |
83 | # Indicates if the token starts a key (string formatting named parameter)
84 | # @rbs return: bool
85 | def key_start?
86 | type == :lbrace || (type == :op && value == "<")
87 | end
88 |
89 | # Indicates if the token finishes a key (string formatting named parameter)
90 | # @rbs return: bool
91 | def key_finish?
92 | type == :rbrace || (type == :op && value == ">")
93 | end
94 |
95 | # Indicates if the token is an identifier (e.g. method name, format specifier, variable name, etc.)
96 | # @rbs return: bool
97 | def identifier?
98 | type == :ident
99 | end
100 |
101 | # Indicates if the token is a method name (i.e. method name or operator)
102 | # @rbs return: bool
103 | def method_name?
104 | identifier? || operator?
105 | end
106 |
107 | # Indicates if the token is an operator
108 | # @rbs return: bool
109 | def operator?
110 | type == :op
111 | end
112 |
113 | # Indicates if the token is a whitespace
114 | # @rbs return: bool
115 | def whitespace?
116 | type == :on_sp
117 | end
118 |
119 | # Indicates if the token is a native String format specifier
120 | # @see Sigils::FORMAT_SPECIFIERS
121 | # @rbs return: bool
122 | def specifier?
123 | identifier? && value in Sigils::FORMAT_SPECIFIERS
124 | end
125 | end
126 | end
127 |
--------------------------------------------------------------------------------
/lib/fmt/tokenizer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | # Ruby source code token extractor
7 | #
8 | # Uses Ripper from Ruby's standard library
9 | # @see https://rubyapi.org/3.4/o/ripper
10 | # @see doc/RIPPER.md (cheetsheet)
11 | #
12 | # @example Ripper token
13 | # [[lineno, column], type, token, state]
14 | # [[Integer, Integer], Symbol, String, Object]
15 | #
16 | class Tokenizer
17 | # Constructor
18 | # @rbs urtext: String -- original source code
19 | def initialize(urtext)
20 | @urtext = urtext.to_s
21 | @tokens = []
22 | end
23 |
24 | attr_reader :urtext # : String -- original source code
25 | attr_reader :tokens # : Array[Object] -- result of tokenization
26 |
27 | # Tokenizes the urtext (original source code)
28 | # @rbs return: Array[Token] -- wrapped ripper tokens
29 | def tokenize
30 | Ripper.lex(urtext).each do |token|
31 | tokens << Token.new(token)
32 | end
33 | tokens
34 | end
35 |
36 | # Returns identifier tokens (typically method names)
37 | # @rbs start: Integer -- start index
38 | # @rbs return: Array[Token]
39 | def identifier_tokens(start: 0)
40 | tokens[start..].each_with_object([]) do |token, memo|
41 | break memo if token.arguments_start?
42 | memo << token if token.identifier?
43 | end
44 | end
45 |
46 | # Returns method tokens (identifiers and operators)
47 | # @rbs start: Integer -- start index
48 | # @rbs return: Array[Token]
49 | def method_name_tokens(start: 0)
50 | identifier_tokens(start: start) + operator_tokens(start: start)
51 | end
52 |
53 | # Returns key (named parameter) tokens
54 | # @rbs start: Integer -- start index
55 | # @rbs return: Array[Token]?
56 | def key_tokens(start: 0)
57 | start = tokens[start..].find(&:key_start?)
58 | identifier = tokens[tokens.index(start)..].find(&:identifier?) if start
59 | finish = tokens[tokens.index(identifier)..].find(&:key_finish?) if identifier
60 | list = [start, identifier, finish].compact
61 |
62 | return [] unless list.size == 3
63 | return [] unless urtext.include?(list.map(&:value).join)
64 |
65 | list
66 | end
67 |
68 | # Returns operator tokens
69 | # @rbs start: Integer -- start index
70 | # @rbs return: Array[Token]
71 | def operator_tokens(start: 0)
72 | tokens[start..].each_with_object([]) do |token, memo|
73 | break memo if token.arguments_start?
74 | memo << token if token.operator?
75 | end
76 | end
77 |
78 | # Returns the argument tokens
79 | # @rbs start: Integer -- start index
80 | # @rbs return: Array[Token]
81 | def argument_tokens(start: 0)
82 | starters = 0
83 | finishers = 0
84 |
85 | tokens[start..].each_with_object([]) do |token, memo|
86 | break memo if starters.positive? && finishers == starters
87 |
88 | starters += 1 if token.arguments_start?
89 | next if starters.zero?
90 |
91 | finishers += 1 if token.arguments_finish?
92 | memo << token
93 | end
94 | end
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/lib/fmt/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | module Fmt
6 | VERSION = "0.3.5"
7 | end
8 |
--------------------------------------------------------------------------------
/sig/generated/fmt.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt.rb with RBS::Inline
2 |
3 | # Extends native Ruby String format specifications
4 | # @see https://ruby-doc.org/3.3.4/format_specifications_rdoc.html
5 | module Fmt
6 | LOCK: untyped
7 |
8 | # Standard error class for Fmt
9 | class Error < StandardError
10 | end
11 |
12 | # Error for formatting failures
13 | class FormatError < Error
14 | end
15 |
16 | # Global registry for storing and retrieving String formatters i.e. Procs
17 | def self.registry: () -> untyped
18 |
19 | # Adds a keypair to the registry
20 | # @rbs key: Array[Class | Module, Symbol] -- key to use
21 | # @rbs overwrite: bool -- overwrite the existing keypair (default: false)
22 | # @rbs block: Proc -- Proc to add (optional, if proc is provided)
23 | # @rbs return: Proc
24 | def self.register: () -> Proc
25 |
26 | # Deletes a keypair from the registry
27 | # @rbs key: Array[Class | Module, Symbol] -- key to delete
28 | # @rbs return: Proc?
29 | def self.unregister: () -> Proc?
30 |
31 | # Executes a block with registry overrides
32 | #
33 | # @note Overrides will temporarily be added to the registry
34 | # and will overwrite existing entries for the duration of the block
35 | # Non overriden entries remain unchanged
36 | #
37 | # @rbs overrides: Hash[Array[Class | Module, Symbol], Proc] -- overrides to apply
38 | # @rbs block: Proc -- block to execute with overrides
39 | # @rbs return: void
40 | def self.with_overrides: () -> void
41 | end
42 |
--------------------------------------------------------------------------------
/sig/generated/fmt/boot.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/boot.rb with RBS::Inline
2 |
3 |
--------------------------------------------------------------------------------
/sig/generated/fmt/lru_cache.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/lru_cache.rb with RBS::Inline
2 |
3 | module Fmt
4 | # A threadsafe fixed-size LRU in-memory cache
5 | # Grows to capacity then evicts the least used entries
6 | #
7 | # @example
8 | # cache = Fmt::Cache.new
9 | #
10 | # cache.put :key, "value"
11 | # cache.get :key
12 | # cache.delete :key
13 | # cache.fetch :key, "default"
14 | # cache.fetch(:key) { "default" }
15 | #
16 | # @example Capacity
17 | # Fmt::Cache.capacity = 10_000
18 | class LRUCache
19 | include MonitorMixin
20 |
21 | DEFAULT_CAPACITY: ::Integer
22 |
23 | # Constructor
24 | # @rbs capacity: Integer -- max capacity (negative values are uncapped, default: 5_000)
25 | # @rbs return: Fmt::Cache
26 | def initialize: (?capacity: Integer) -> Fmt::Cache
27 |
28 | # The cache max capacity (number of entries)
29 | # @rbs return: Integer
30 | def capacity: () -> Integer
31 |
32 | # Set the max capacity (number of entries)
33 | # @rbs capacity: Integer -- new max capacity
34 | # @rbs return: Integer -- new max capacity
35 | def capacity=: (Integer capacity) -> Integer
36 |
37 | # Indicates if the cache is capped
38 | # @rbs return: bool
39 | def capped?: () -> bool
40 |
41 | # Clears the cache
42 | # @rbs return: void
43 | def clear: () -> void
44 |
45 | # Deletes the entry for the specified key
46 | # @rbs key: Object -- key to delete
47 | # @rbs return: Object? -- the deleted value
48 | def delete: (Object key) -> Object?
49 |
50 | # Fetches the value for the specified key
51 | # Writes the default value if the key is not found
52 | # @rbs key: Object -- key to fetch
53 | # @rbs default: Object -- default value to write
54 | # @rbs block: Proc -- block to call to get the default value
55 | # @rbs return: Object -- value
56 | def fetch: (Object key, ?Object default) ?{ (?) -> untyped } -> Object
57 |
58 | # Fetches a value from the cache without synchronization (not thread safe)
59 | # @rbs key: Object -- key to fetch
60 | # @rbs default: Object -- default value to write
61 | # @rbs block: Proc -- block to call to get the default value
62 | # @rbs return: Object -- value
63 | def fetch_unsafe: (Object key, ?Object default) ?{ (?) -> untyped } -> Object
64 |
65 | # Indicates if the cache is full
66 | # @rbs return: bool
67 | def full?: () -> bool
68 |
69 | # Retrieves the value for the specified key
70 | # @rbs key: Object -- key to retrieve
71 | def get: (Object key) -> untyped
72 |
73 | # Cache keys
74 | # @rbs return: Array[Object]
75 | def keys: () -> Array[Object]
76 |
77 | # Indicates if the cache contains the specified key
78 | # @rbs key: Object -- key to check
79 | # @rbs return: bool
80 | def key?: (Object key) -> bool
81 |
82 | # Stores the value for the specified key
83 | # @rbs key: Object -- key to store
84 | # @rbs value: Object -- value to store
85 | # @rbs return: Object -- value
86 | def put: (Object key, Object value) -> Object
87 |
88 | # Resets the cache capacity to the default
89 | # @rbs return: Integer -- capacity
90 | def reset_capacity: () -> Integer
91 |
92 | # The current size of the cache (number of entries)
93 | # @rbs return: Integer
94 | def size: () -> Integer
95 |
96 | # Returns a Hash with only the given keys
97 | # @rbs keys: Array[Object] -- keys to include
98 | # @rbs return: Hash[Object, Object]
99 | def slice: (*untyped keys) -> Hash[Object, Object]
100 |
101 | # Hash representation of the cache
102 | # @rbs return: Hash[Object, Proc]
103 | def to_h: () -> Hash[Object, Proc]
104 |
105 | # Cache values
106 | # @rbs return: Array[Object]
107 | def values: () -> Array[Object]
108 |
109 | # Executes a block with a synchronized mutex
110 | # @rbs block: Proc -- block to execute
111 | def lock: () ?{ (?) -> untyped } -> untyped
112 |
113 | private
114 |
115 | attr_reader store: untyped
116 |
117 | # Moves the key to the end keeping it fresh
118 | # @rbs key: Object -- key to reposition
119 | # @rbs return: Object -- value
120 | def reposition: (Object key) -> Object
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/sig/generated/fmt/mixins/matchable.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/mixins/matchable.rb with RBS::Inline
2 |
3 | module Fmt
4 | module Matchable
5 | # Hash representation of the Object (required for pattern matching)
6 | # @rbs return: Hash[Symbol, Object]
7 | def to_h: () -> Hash[Symbol, Object]
8 |
9 | # Returns a Hash representation of the object limited to the given keys
10 | # @rbs keys: Array[Symbol] -- keys to include
11 | # @rbs return: Hash[Symbol, Object]
12 | def deconstruct_keys: (?Array[Symbol] keys) -> Hash[Symbol, Object]
13 |
14 | # Returns an Array representation of the object
15 | # @rbs return: Array[Object]
16 | def deconstruct: () -> Array[Object]
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/sig/generated/fmt/models/arguments.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/models/arguments.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Represents arguments for a method call
5 | #
6 | # Arguments are comprised of:
7 | # 1. args: Array[Object]
8 | # 2. kwargs: Hash[Symbol, Object]
9 | class Arguments < Model
10 | # Constructor
11 | # @rbs ast: Node
12 | def initialize: (Node ast) -> untyped
13 |
14 | attr_reader args: untyped
15 |
16 | attr_reader kwargs: untyped
17 |
18 | # Hash representation of the model (required for pattern matching)
19 | # @rbs return: Hash[Symbol, Object]
20 | def to_h: () -> Hash[Symbol, Object]
21 |
22 | # Processes an arguments AST node
23 | # @rbs node: Node
24 | # @rbs return: void
25 | def on_arguments: (Node node) -> void
26 |
27 | # Processes a tokens AST node
28 | # @rbs node: Node
29 | # @rbs return: void
30 | def on_tokens: (Node node) -> void
31 |
32 | # Processes a keyword AST node
33 | # @rbs node: Node
34 | # @rbs return: nil | true | false | Object
35 | def on_kw: (Node node) -> (nil | true | false | Object)
36 |
37 | # Processes a string AST node
38 | # @rbs node: Node
39 | # @rbs return: String
40 | def on_tstring_content: (Node node) -> String
41 |
42 | # Processes a symbol AST Node
43 | # @rbs node: Node
44 | # @rbs return: Symbol
45 | def on_symbol: (Node node) -> Symbol
46 |
47 | # Processes a symbol start AST Node
48 | # @rbs node: Node
49 | # @rbs return: void
50 | def on_symbeg: (Node node) -> void
51 |
52 | # Processes an identifier AST Node
53 | # @rbs node: Node
54 | # @rbs return: Symbol?
55 | def on_ident: (Node node) -> Symbol?
56 |
57 | # Processes an integer AST node
58 | # @rbs node: Node
59 | # @rbs return: Integer
60 | def on_int: (Node node) -> Integer
61 |
62 | # Processes a float AST node
63 | # @rbs node: Node
64 | # @rbs return: Float
65 | def on_float: (Node node) -> Float
66 |
67 | # Processes a rational AST node
68 | # @rbs node: Node
69 | # @rbs return: Rational
70 | def on_rational: (Node node) -> Rational
71 |
72 | # Processes an imaginary (complex) AST node
73 | # @rbs node: Node
74 | # @rbs return: Complex
75 | def on_imaginary: (Node node) -> Complex
76 |
77 | # Processes a left bracket AST node
78 | # @rbs node: Node
79 | # @rbs return: Array
80 | def on_lbracket: (Node node) -> Array
81 |
82 | # Processes a left brace AST node
83 | # @rbs node: Node
84 | # @rbs return: Hash
85 | def on_lbrace: (Node node) -> Hash
86 |
87 | # Process a label (hash key) AST node
88 | # @rbs node: Node
89 | # @rbs return: void
90 | def on_label: (Node node) -> void
91 |
92 | private
93 |
94 | # Assigns a value to the receiver
95 | # @rbs value: Object -- value to assign
96 | # @rbs label: Symbol? -- label to use (if applicable)
97 | # @rbs return: Object
98 | def assign: (Object value, ?label: Symbol?) -> Object
99 |
100 | # Receiver that the processed value will be assigned to
101 | # @rbs label: Symbol? -- label to use (if applicable)
102 | # @rbs return: Array | Hash
103 | def receiver: (?label: Symbol?) -> (Array | Hash)
104 |
105 | # Finds the receiver that the processed value will be assigned to
106 | # @rbs obj: Object
107 | # @rbs return: Array? | Hash?
108 | def find_receiver: (Object obj) -> (Array? | Hash?)
109 |
110 | # Indicates if the value is a composite type (Array or Hash)
111 | # @rbs value: Object -- value to check
112 | # @rbs return: bool
113 | def composite?: (Object value) -> bool
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/sig/generated/fmt/models/embed.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/models/embed.rb with RBS::Inline
2 |
3 | module Fmt
4 | class Embed < Model
5 | attr_reader key: untyped
6 |
7 | attr_reader placeholder: untyped
8 |
9 | attr_reader template: untyped
10 |
11 | # Hash representation of the model (required for pattern matching)
12 | # @rbs return: Hash[Symbol, Object]
13 | def to_h: () -> Hash[Symbol, Object]
14 |
15 | # Processes an embed AST node
16 | # @rbs node: Node
17 | # @rbs return: void
18 | def on_embed: (Node node) -> void
19 |
20 | # Processes a key AST node
21 | # @rbs node: Node
22 | # @rbs return: void
23 | def on_key: (Node node) -> void
24 |
25 | # Processes a placeholder AST node
26 | # @rbs node: Node
27 | # @rbs return: void
28 | def on_placeholder: (Node node) -> void
29 |
30 | # Processes a template AST node
31 | # @rbs node: Node
32 | def on_template: (Node node) -> untyped
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/sig/generated/fmt/models/macro.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/models/macro.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Represents an uninvoked method call
5 | #
6 | # A Macro is comprised of:
7 | # 1. name: Symbol
8 | # 2. arguments: Arguments
9 | class Macro < Model
10 | attr_reader name: untyped
11 |
12 | attr_reader arguments: untyped
13 |
14 | # Constructor
15 | # @rbs ast: Node
16 | def initialize: (Node ast) -> untyped
17 |
18 | # Hash representation of the model (required for pattern matching)
19 | # @rbs return: Hash[Symbol, Object]
20 | def to_h: () -> Hash[Symbol, Object]
21 |
22 | # Processes a macro AST node
23 | # @rbs node: Node
24 | # @rbs return: void
25 | def on_macro: (Node node) -> void
26 |
27 | # Processes a procedure AST node
28 | # @rbs node: Node
29 | # @rbs return: void
30 | def on_name: (Node node) -> void
31 |
32 | # Processes an arguments AST node
33 | # @rbs node: Node
34 | # @rbs return: void
35 | def on_arguments: (Node node) -> void
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/sig/generated/fmt/models/model.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/models/model.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Superclass for all models
5 | # @note Models are constructed from AST nodes
6 | class Model
7 | include AST::Processor::Mixin
8 |
9 | include Matchable
10 |
11 | # Constructor
12 | # @rbs ast: Node
13 | def initialize: (Node ast) -> untyped
14 |
15 | attr_reader ast: untyped
16 |
17 | attr_reader urtext: untyped
18 |
19 | attr_reader source: untyped
20 |
21 | # Model inspection
22 | # @rbs return: String
23 | def inspect: () -> String
24 |
25 | # Indicates if a given AST node is the same AST used to construct the model
26 | # @rbs node: Node
27 | # @rbs return: bool
28 | def self?: (Node node) -> bool
29 |
30 | # Hash representation of the model (required for pattern matching)
31 | # @note Subclasses should override this method and call: super.merge(**)
32 | # @rbs return: Hash[Symbol, Object]
33 | def to_h: () -> Hash[Symbol, Object]
34 |
35 | private
36 |
37 | # Hash of instance variables for inspection
38 | # @rbs return: Hash[String, Object]
39 | def inspectable_properties: () -> Hash[String, Object]
40 |
41 | # String of inspectable properties for inspection
42 | # @rbs return: String
43 | def inspect_properties: () -> String
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/sig/generated/fmt/models/pipeline.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/models/pipeline.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Represents a series of Macros
5 | #
6 | # A Pipeline is comprised of:
7 | # 1. macros: Array[Macro]
8 | #
9 | # @note Pipelines are processed in sequence (left to right)
10 | class Pipeline < Model
11 | # Constructor
12 | # @rbs ast: Node
13 | def initialize: (Node ast) -> untyped
14 |
15 | attr_reader macros: untyped
16 |
17 | # Hash representation of the model (required for pattern matching)
18 | # @rbs return: Hash[Symbol, Object]
19 | def to_h: () -> Hash[Symbol, Object]
20 |
21 | # Processes a pipeline AST node
22 | # @rbs node: Node
23 | # @rbs return: void
24 | def on_pipeline: (Node node) -> void
25 |
26 | # Processes a macro AST node
27 | # @rbs node: Node
28 | # @rbs return: void
29 | def on_macro: (Node node) -> void
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/sig/generated/fmt/models/template.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/models/template.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Represents a formattable string
5 | #
6 | # A Template is comprised of:
7 | # 1. embeds: Array[Template] -- embedded templates
8 | # 2. pipelines :: Array[Pipeline] -- sets of Macros
9 | #
10 | # @note Embeds are processed from inner to outer
11 | class Template < Model
12 | # Constructor
13 | # @rbs ast: Node
14 | def initialize: (Node ast) -> untyped
15 |
16 | attr_reader embeds: untyped
17 |
18 | attr_reader pipelines: untyped
19 |
20 | # @rbs return: Hash[Symbol, Object]
21 | def to_h: () -> Hash[Symbol, Object]
22 |
23 | def on_template: (untyped node) -> untyped
24 |
25 | def on_embeds: (untyped node) -> untyped
26 |
27 | def on_embed: (untyped node) -> untyped
28 |
29 | def on_pipelines: (untyped node) -> untyped
30 |
31 | def on_pipeline: (untyped node) -> untyped
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/sig/generated/fmt/node.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/node.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Extends behavior of AST::Node
5 | class Node < AST::Node
6 | extend Forwardable
7 |
8 | # Finds all Node child nodes
9 | # @rbs node: Node -- node to search
10 | # @rbs return: Array[Node]
11 | def self.node_children: (Node node) -> Array[Node]
12 |
13 | # Recursively finds all Nodes in the tree
14 | # @rbs node: Node -- node to search
15 | # @rbs return: Array[Node]
16 | def self.node_descendants: (Node node) -> Array[Node]
17 |
18 | # Constructor
19 | # @rbs type: Symbol
20 | # @rbs children: Array[Node]
21 | # @rbs properties: Hash[Symbol, Object]
22 | def initialize: (Symbol type, ?Array[Node] children, ?Hash[Symbol, Object] properties) -> untyped
23 |
24 | attr_reader properties: untyped
25 |
26 | # Recursively searches the tree for a descendant node
27 | # @rbs types: Array[Object] -- node types to find
28 | # @rbs return: Node?
29 | def dig: (*untyped types) -> Node?
30 |
31 | # Finds the first child node of the specified type
32 | # @rbs type: Object -- node type to find
33 | # @rbs return: Node?
34 | def find: (Object type) -> Node?
35 |
36 | # Flattens Node descendants into a one dimensional array
37 | # @rbs return: Array[Node]
38 | def flatten: () -> Array[Node]
39 |
40 | # Finds all child nodes of the specified type
41 | # @rbs type: Object -- node type to select
42 | # @rbs return: Node?
43 | def select: (Object type) -> Node?
44 |
45 | # String representation of the node (AST)
46 | # @rbs squish: bool -- remove extra whitespace
47 | # @rbs return: String
48 | def to_s: (?squish: bool) -> String
49 |
50 | private
51 |
52 | # Finds all Node child nodes
53 | # @rbs return: Array[Node]
54 | def node_children: () -> Array[Node]
55 |
56 | # Recursively finds all Node nodes in the tree
57 | # @rbs return: Array[Node]
58 | def node_descendants: () -> Array[Node]
59 |
60 | # Defines accessor methods for properties on the receiver
61 | # @rbs properties: Hash[Symbol, Object] -- exposed as instance methods
62 | def define_properties: (Hash[Symbol, Object] properties) -> untyped
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/sig/generated/fmt/parsers/arguments_parser.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/parsers/arguments_parser.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Parses arguments from a string and builds an AST (Abstract Syntax Tree)
5 | class ArgumentsParser < Parser
6 | # Constructor
7 | # @rbs tokens: Array[Token] -- wrapped ripper tokens
8 | def initialize: (?Array[Token] tokens) -> untyped
9 |
10 | attr_reader tokens: untyped
11 |
12 | # Parses the urtext (original source code)
13 | # @rbs return: Node -- AST (Abstract Syntax Tree)
14 | def parse: () -> Node
15 |
16 | # Extracts components for building the AST (Abstract Syntax Tree)
17 | # @rbs return: Hash[Symbol, Object] -- extracted components
18 | def extract: () -> Hash[Symbol, Object]
19 |
20 | # Transforms extracted components into an AST (Abstract Syntax Tree)
21 | # @rbs tokens: Array[Token] -- extracted tokens
22 | # @rbs return: Node -- AST (Abstract Syntax Tree)
23 | def transform: (tokens: Array[Token]) -> Node
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/sig/generated/fmt/parsers/embed_parser.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/parsers/embed_parser.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Parses embeds from a string and builds an AST (Abstract Syntax Tree)
5 | class EmbedParser < Parser
6 | # Constructor
7 | # @rbs urtext: String -- original source code
8 | # @rbs key: Symbol -- key for embed
9 | # @rbs placeholder: String -- placeholder for embed
10 | def initialize: (?String urtext, key: Symbol, placeholder: String) -> untyped
11 |
12 | attr_reader urtext: untyped
13 |
14 | attr_reader key: untyped
15 |
16 | attr_reader placeholder: untyped
17 |
18 | # Parses the urtext (original source code)
19 | # @rbs return: Node -- AST (Abstract Syntax Tree)
20 | def parse: () -> Node
21 |
22 | # Extracts components for building the AST (Abstract Syntax Tree)
23 | # @rbs return: Hash[Symbol, Object] -- extracted components
24 | def extract: () -> Hash[Symbol, Object]
25 |
26 | # Transforms extracted components into an AST (Abstract Syntax Tree)
27 | # @rbs return: Node -- AST (Abstract Syntax Tree)
28 | def transform: (source: untyped) -> Node
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/sig/generated/fmt/parsers/macro_parser.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/parsers/macro_parser.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Parses a macro from a string and builds an AST (Abstract Syntax Tree)
5 | class MacroParser < Parser
6 | # Constructor
7 | # @rbs urtext: String -- original source code
8 | def initialize: (?String urtext) -> untyped
9 |
10 | attr_reader urtext: untyped
11 |
12 | # Parses the urtext (original source code)
13 | # @rbs return: Node -- AST (Abstract Syntax Tree)
14 | def parse: () -> Node
15 |
16 | # Extracts components for building the AST (Abstract Syntax Tree)
17 | # @rbs return: Hash[Symbol, Object] -- extracted components
18 | def extract: () -> Hash[Symbol, Object]
19 |
20 | # Transforms extracted components into an AST (Abstract Syntax Tree)
21 | # @rbs method: Symbol?
22 | # @rbs arguments_tokens: Array[Token] -- arguments tokens
23 | # @rbs return: Node -- AST (Abstract Syntax Tree)
24 | def transform: (method: Symbol?, arguments_tokens: Array[Token]) -> Node
25 |
26 | private
27 |
28 | # Tokenizes source code
29 | # @rbs code: String -- source code to tokenize
30 | # @rbs return: Array[Token] -- wrapped ripper tokens
31 | def tokenize: (String code) -> Array[Token]
32 |
33 | # Indicates if there is a set of arguments in the tokens
34 | # @rbs tokens: Array[Token] -- tokens to check
35 | # @rbs return: bool
36 | def arguments?: (Array[Token] tokens) -> bool
37 |
38 | # Indicates if arguments have started
39 | # @rbs tokens: Array[Token] -- tokens to check
40 | # @rbs return: bool
41 | def arguments_started?: (Array[Token] tokens) -> bool
42 |
43 | # Indicates if arguments have finished
44 | # @note Call this after a whitespace has been detected
45 | # @rbs tokens: Array[Token] -- tokens to check
46 | # @rbs return: bool
47 | def arguments_finished?: (Array[Token] tokens) -> bool
48 |
49 | # Indicates if a macro token array is complete or finished
50 | # @note Call this after a whitespace has been detected
51 | # @rbs tokens: Array[Token] -- tokens to check
52 | # @rbs return: bool
53 | def finished?: (Array[Token] tokens) -> bool
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/sig/generated/fmt/parsers/parser.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/parsers/parser.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Responsible for parsing various inputs and returning an AST (Abstract Syntax Tree)
5 | #
6 | # Mechanics are similar to an ETL pipeline (Extract, Transform, Load), however,
7 | # parsers only handle extracting and transforming.
8 | #
9 | # Loading is handled by AST processors (Models)
10 | # @see lib/fmt/models/
11 | class Parser
12 | Cache: untyped
13 |
14 | # Escapes a string for use in a regular expression
15 | # @rbs value: String -- string to escape
16 | # @rbs return: String -- escaped string
17 | def self.esc: (String value) -> String
18 |
19 | # Parses input passed to the constructor and returns an AST (Abstract Syntax Tree)
20 | #
21 | # 1. Extract components
22 | # 2. Transform to AST
23 | #
24 | # @note Subclasses must implement the extract and transform methods
25 | #
26 | # @rbs return: Node -- AST (Abstract Syntax Tree)
27 | def parse: () -> Node
28 |
29 | # Extracts components for building the AST (Abstract Syntax Tree)
30 | # @rbs return: Hash[Symbol, Object] -- extracted components
31 | def extract: () -> Hash[Symbol, Object]
32 |
33 | # Transforms extracted components into an AST (Abstract Syntax Tree)
34 | # @rbs kwargs: Hash[Symbol, Object] -- extracted components
35 | # @rbs return: Node -- AST (Abstract Syntax Tree)
36 | def transform: (**untyped kwargs) -> Node
37 |
38 | # Cache helper that fetches a value from the cache
39 | # @rbs key: String -- cache key
40 | # @rbs block: Proc -- block to execute if the value is not found in the cache
41 | # @rbs return: Object
42 | def cache: (String key) ?{ (?) -> untyped } -> Object
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/sig/generated/fmt/parsers/pipeline_parser.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/parsers/pipeline_parser.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Parses a pipeline from a string and builds an AST (Abstract Syntax Tree)
5 | class PipelineParser < Parser
6 | # Constructor
7 | # @rbs urtext: String -- original source code
8 | def initialize: (?String urtext) -> untyped
9 |
10 | attr_reader urtext: untyped
11 |
12 | # Parses the urtext (original source code)
13 | # @rbs return: Node -- AST (Abstract Syntax Tree)
14 | def parse: () -> Node
15 |
16 | # Extracts components for building the AST (Abstract Syntax Tree)
17 | # @rbs return: Hash[Symbol, Object] -- extracted components
18 | def extract: () -> Hash[Symbol, Object]
19 |
20 | # Transforms extracted components into an AST (Abstract Syntax Tree)
21 | # @rbs macros: Array[Array[Token]] -- extracted macro tokens
22 | # @rbs return: Node -- AST (Abstract Syntax Tree)
23 | def transform: (macros: Array[Array[Token]]) -> Node
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/sig/generated/fmt/parsers/template_parser.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/parsers/template_parser.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Parses a template from a string and builds an AST (Abstract Syntax Tree)
5 | class TemplateParser < Parser
6 | EMBED_PEEK: ::Regexp
7 |
8 | PIPELINE_PEEK: ::Regexp
9 |
10 | PERCENT_LITERAL: ::Regexp
11 |
12 | WHITESPACE: ::Regexp
13 |
14 | # Constructor
15 | # @rbs urtext: String -- original source code
16 | def initialize: (?String urtext) -> untyped
17 |
18 | attr_reader urtext: untyped
19 |
20 | # Parses the urtext (original source code)
21 | # @rbs return: Node -- AST (Abstract Syntax Tree)
22 | def parse: () -> Node
23 |
24 | # Extracts components for building the AST (Abstract Syntax Tree)
25 | # @note Extraction is delegated to the PipelineParser and EmbedParser in transform
26 | # @rbs return: Hash
27 | def extract: () -> Hash
28 |
29 | # Transforms extracted components into an AST (Abstract Syntax Tree)
30 | # @rbs embeds: Array[Hash] -- extracted embeds
31 | # @rbs pipelines: Array[String] -- extracted pipelines
32 | # @rbs source: String -- parsed source code
33 | # @rbs return: Node -- AST (Abstract Syntax Tree)
34 | def transform: (embeds: Array[Hash], pipelines: Array[String], source: String) -> Node
35 |
36 | private
37 |
38 | # Extracts the next embed with the scanner
39 | # @rbs scanner: StringScanner -- scanner to extract from
40 | # @rbs return: String? -- extracted embed
41 | def extract_next_embed: (StringScanner scanner) -> String?
42 |
43 | # Extracts embed metadata from the source
44 | # @rbs return: Array[Hash] -- extracted embeds
45 | def extract_embeds: (untyped source) -> Array[Hash]
46 |
47 | # Extracts the next pipeline with the scanner
48 | # @rbs scanner: StringScanner -- scanner to extract from
49 | # @rbs return: String? -- extracted pipeline
50 | def extract_next_pipeline: (StringScanner scanner) -> String?
51 |
52 | # Extracts pipelines from the source
53 | # @rbs source: String -- source code to extract pipelines from
54 | # @rbs return: Array[String] -- extracted pipelines
55 | def extract_pipelines: (String source) -> Array[String]
56 |
57 | # Indicates if arguments are balances in the given list of chars
58 | # @rbs value: String -- value to check
59 | # @rbs return: bool
60 | def arguments_balanced?: (String value) -> bool
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/sig/generated/fmt/refinements/kernel_refinement.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/refinements/kernel_refinement.rb with RBS::Inline
2 |
3 | module Fmt
4 | module KernelRefinement
5 | # Formats an object with Fmt
6 | # @rbs object [Object] -- object to format (coerced to String)
7 | # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
8 | # @rbs return [String] -- formatted text
9 | def fmt: ([ Object ] object, *untyped pipeline) -> [ String ]
10 |
11 | # Formats an object with Fmt and prints to STDOUT
12 | # @rbs object [Object] -- object to format (coerced to String)
13 | # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
14 | # @rbs return void
15 | def fmt_print: ([ Object ] object, *untyped pipeline) -> void
16 |
17 | # Formats an object with Fmt and puts to STDOUT
18 | # @rbs object [Object] -- object to format (coerced to String)
19 | # @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
20 | # @rbs return void
21 | def fmt_puts: ([ Object ] object, *untyped pipeline) -> void
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/sig/generated/fmt/registries/native_registry.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/registries/native_registry.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Extends native Ruby String format specifications with native Ruby methods
5 | # @see https://ruby-doc.org/3.3.4/format_specifications_rdoc.html
6 | class NativeRegistry < Registry
7 | SUPPORTED_CLASSES: untyped
8 |
9 | # Constructor
10 | def initialize: () -> untyped
11 |
12 | private
13 |
14 | # Array of supported method names for a Class
15 | # @rbs klass: Class
16 | # @rbs return: Array[Symbol]
17 | def supported_method_names: (Class klass) -> Array[Symbol]
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/sig/generated/fmt/registries/rainbow_registry.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/registries/rainbow_registry.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Extends native Ruby String format specifications with Rainbow methods
5 | # @see https://ruby-doc.org/3.3.4/format_specifications_rdoc.html
6 | # @note Rainbow macros convert the Object to a String
7 | class RainbowRegistry < Registry
8 | # Constructor
9 | def initialize: () -> untyped
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/sig/generated/fmt/registries/registry.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/registries/registry.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Registry for storing and retrieving String formatters i.e. Procs
5 | class Registry
6 | extend Forwardable
7 |
8 | INSTANCE_VAR: ::Symbol
9 |
10 | # Constructor
11 | def initialize: () -> untyped
12 |
13 | # Indicates if a method name is registered for any Class
14 | # @rbs method_name: Symbol -- method name to check
15 | # @rbs return: bool
16 | def any?: (Symbol method_name) -> bool
17 |
18 | # Indicates if a method name is unregistered
19 | # @rbs method_name: Symbol -- method name to check
20 | # @rbs return: bool
21 | def none?: (Symbol method_name) -> bool
22 |
23 | # Adds a keypair to the registry
24 | # @rbs key: Array[Class | Module, Symbol] -- key to use
25 | # @rbs overwrite: bool -- overwrite the existing keypair (default: false)
26 | # @rbs block: Proc -- Proc to add (optional, if proc is provided)
27 | # @rbs return: Proc
28 | def add: (Array[Class | Module, Symbol] key, ?overwrite: bool) ?{ (?) -> untyped } -> Proc
29 |
30 | # Deletes a keypair from the registry
31 | # @rbs key: Array[Class | Module, Symbol] -- key to delete
32 | # @rbs return: Proc?
33 | def delete: (Array[Class | Module, Symbol] key) -> Proc?
34 |
35 | # Fetches a Proc from the registry
36 | # @rbs key: Array[Class | Module, Symbol] -- key to retrieve
37 | # @rbs callable: Proc -- Proc to use if the key is not found (optional, if block is provided)
38 | # @rbs block: Proc -- block to use if the key is not found (optional, if proc is provided)
39 | # @rbs return: Proc
40 | def fetch: (Array[Class | Module, Symbol] key, ?callable: Proc) ?{ (?) -> untyped } -> Proc
41 |
42 | # Retrieves the registered key for a Proc
43 | # @rbs callable: Proc -- Proc to retrieve the key for
44 | # @rbs return: Symbol?
45 | def key_for: (Proc callable) -> Symbol?
46 |
47 | # Merges another registry into this one
48 | # @rbs other: Fmt::Registry -- other registry to merge
49 | # @rbs return: Fmt::Registry
50 | def merge!: (Fmt::Registry other) -> Fmt::Registry
51 |
52 | # Executes a block with registry overrides
53 | #
54 | # @note Overrides will temporarily be added to the registry
55 | # and will overwrite existing entries for the duration of the block
56 | # Non overriden entries remain unchanged
57 | #
58 | # @rbs overrides: Hash[Array[Class | Module, Symbol], Proc] -- overrides to apply
59 | # @rbs block: Proc -- block to execute with overrides
60 | # @rbs return: void
61 | def with_overrides: (Hash[Array[Class | Module, Symbol], Proc] overrides) ?{ (?) -> untyped } -> void
62 |
63 | attr_reader store: untyped
64 |
65 | # Hash of registered method names
66 | # @rbs return: Hash[Symbol, TrueClass]
67 | def method_names: () -> Hash[Symbol, TrueClass]
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/sig/generated/fmt/renderer.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/renderer.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Renders templates to a formatted string
5 | class Renderer
6 | PIPELINE_START: untyped
7 |
8 | # Constructor
9 | # @rbs template: Template
10 | def initialize: (Template template) -> untyped
11 |
12 | attr_reader template: untyped
13 |
14 | # Renders the template to a string
15 | # @note Positional and Keyword arguments are mutually exclusive
16 | # @rbs args: Array[Object] -- positional arguments (user provided)
17 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
18 | # @rbs return: String -- rendered template
19 | def render: (*untyped args, **untyped kwargs) -> String
20 |
21 | private
22 |
23 | # Renders all template embeds
24 | # @rbs args: Array[Object] -- positional arguments (user provided)
25 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
26 | # @rbs &block: Proc -- block executed for each embed (signature: Proc(Embed, String))
27 | def render_embeds: (*untyped args, **untyped kwargs) -> untyped
28 |
29 | # Renders all template pipelines
30 | # @rbs args: Array[Object] -- positional arguments (user provided)
31 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
32 | # @rbs block: Proc -- block executed for each pipeline (signature: Proc(Pipeline, String))
33 | def render_pipelines: (*untyped args, **untyped kwargs) -> untyped
34 |
35 | # Renders a single pipeline
36 | # @rbs pipeline: Pipeline -- pipeline to render
37 | # @rbs args: Array[Object] -- positional arguments (user provided)
38 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
39 | # @rbs return: String
40 | def render_pipeline: (Pipeline pipeline, *untyped args, **untyped kwargs) -> String
41 |
42 | # Invokes a macro
43 | # @rbs context: Object -- self in callable (Proc)
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)
47 | # @rbs return: Object -- result
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
54 |
55 | # Raises an invocation error if/when Proc invocations fail
56 | # @rbs macro: Macro -- macro that failed
57 | # @rbs args: Array[Object] -- positional arguments (user provided)
58 | # @rbs cause: Exception -- exception that caused the error
59 | # @rbs kwargs: Hash[Symbol, Object] -- keyword arguments (user provided)
60 | # @rbs return: void
61 | def raise_format_error: (Macro macro, *untyped args, cause: Exception, **untyped kwargs) -> void
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/sig/generated/fmt/sigils.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/sigils.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Common Fmt sigils (used in String templates)
5 | class Sigils
6 | # Native Ruby format specifiers
7 | # @see https://docs.ruby-lang.org/en/master/format_specifications_rdoc.html
8 | FORMAT_PREFIX: ::String
9 |
10 | FORMAT_SPECIFIERS: untyped
11 |
12 | FORMAT_FLAGS: untyped
13 |
14 | FORMAT_METHOD: ::Symbol
15 |
16 | KEY_PREFIXES: untyped
17 |
18 | KEY_SUFFIXES: untyped
19 |
20 | ARGS_PREFIX: ::String
21 |
22 | ARGS_SUFFIX: ::String
23 |
24 | PIPE_OPERATOR: ::String
25 |
26 | EMBED_PREFIX: ::String
27 |
28 | EMBED_SUFFIX: ::String
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/sig/generated/fmt/token.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/token.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Convenience wrapper for Ripper tokens
5 | #
6 | # @see https://rubyapi.org/3.4/o/ripper
7 | # @see doc/RIPPER.md (cheetsheet)
8 | #
9 | # @example Ripper Token
10 | # [[lineno, column], type, token, state]
11 | # [[Integer, Integer], Symbol, String, Object]
12 | class Token
13 | include Matchable
14 |
15 | # Constructor
16 | # @rbs ripper_token: Array[[Integer, Integer], Symbol, String, Object] -- Ripper token
17 | def initialize: (Array[[ Integer, Integer ], Symbol, String, Object] ripper_token) -> untyped
18 |
19 | attr_reader ripper_token: untyped
20 |
21 | attr_reader lineno: untyped
22 |
23 | attr_reader column: untyped
24 |
25 | attr_reader type: untyped
26 |
27 | attr_reader token: untyped
28 |
29 | attr_reader state: untyped
30 |
31 | # Returns a Hash representation of the token
32 | # @rbs return: Hash[Symbol, Object]
33 | def to_h: () -> Hash[Symbol, Object]
34 |
35 | # Returns a Hash representation of the token limited to the given keys
36 | # @rbs keys: Array[Symbol] -- keys to include
37 | # @rbs return: Hash[Symbol, Object]
38 | def deconstruct_keys: (?Array[Symbol] keys) -> Hash[Symbol, Object]
39 |
40 | # Indicates if the token is a left paren (i.e. start of arguments)
41 | # @rbs return: bool
42 | def arguments_start?: () -> bool
43 |
44 | # Indicates if the token is a right paren (i.e. end of arguments)
45 | # @rbs return: bool
46 | def arguments_finish?: () -> bool
47 |
48 | # Indicates if the token starts a key (string formatting named parameter)
49 | # @rbs return: bool
50 | def key_start?: () -> bool
51 |
52 | # Indicates if the token finishes a key (string formatting named parameter)
53 | # @rbs return: bool
54 | def key_finish?: () -> bool
55 |
56 | # Indicates if the token is an identifier (e.g. method name, format specifier, variable name, etc.)
57 | # @rbs return: bool
58 | def identifier?: () -> bool
59 |
60 | # Indicates if the token is a method name (i.e. method name or operator)
61 | # @rbs return: bool
62 | def method_name?: () -> bool
63 |
64 | # Indicates if the token is an operator
65 | # @rbs return: bool
66 | def operator?: () -> bool
67 |
68 | # Indicates if the token is a whitespace
69 | # @rbs return: bool
70 | def whitespace?: () -> bool
71 |
72 | # Indicates if the token is a native String format specifier
73 | # @see Sigils::FORMAT_SPECIFIERS
74 | # @rbs return: bool
75 | def specifier?: () -> bool
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/sig/generated/fmt/tokenizer.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/tokenizer.rb with RBS::Inline
2 |
3 | module Fmt
4 | # Ruby source code token extractor
5 | #
6 | # Uses Ripper from Ruby's standard library
7 | # @see https://rubyapi.org/3.4/o/ripper
8 | # @see doc/RIPPER.md (cheetsheet)
9 | #
10 | # @example Ripper token
11 | # [[lineno, column], type, token, state]
12 | # [[Integer, Integer], Symbol, String, Object]
13 | class Tokenizer
14 | # Constructor
15 | # @rbs urtext: String -- original source code
16 | def initialize: (String urtext) -> untyped
17 |
18 | attr_reader urtext: untyped
19 |
20 | attr_reader tokens: untyped
21 |
22 | # Tokenizes the urtext (original source code)
23 | # @rbs return: Array[Token] -- wrapped ripper tokens
24 | def tokenize: () -> Array[Token]
25 |
26 | # Returns identifier tokens (typically method names)
27 | # @rbs start: Integer -- start index
28 | # @rbs return: Array[Token]
29 | def identifier_tokens: (?start: Integer) -> Array[Token]
30 |
31 | # Returns method tokens (identifiers and operators)
32 | # @rbs start: Integer -- start index
33 | # @rbs return: Array[Token]
34 | def method_name_tokens: (?start: Integer) -> Array[Token]
35 |
36 | # Returns key (named parameter) tokens
37 | # @rbs start: Integer -- start index
38 | # @rbs return: Array[Token]?
39 | def key_tokens: (?start: Integer) -> Array[Token]?
40 |
41 | # Returns operator tokens
42 | # @rbs start: Integer -- start index
43 | # @rbs return: Array[Token]
44 | def operator_tokens: (?start: Integer) -> Array[Token]
45 |
46 | # Returns the argument tokens
47 | # @rbs start: Integer -- start index
48 | # @rbs return: Array[Token]
49 | def argument_tokens: (?start: Integer) -> Array[Token]
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/sig/generated/fmt/version.rbs:
--------------------------------------------------------------------------------
1 | # Generated from lib/fmt/version.rb with RBS::Inline
2 |
3 | module Fmt
4 | VERSION: ::String
5 | end
6 |
--------------------------------------------------------------------------------
/test/refinements/test_kernel_refinement.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | require "test_helper"
6 |
7 | class TestKernelRefinement < Minitest::Test
8 | using Fmt::KernelRefinement
9 |
10 | def test_fmt
11 | assert_equal "\e[1mHello, World!\e[0m", fmt("Hello, World!", :bold)
12 | assert_equal "\e[4mhello\e[0m", fmt(:hello, :underline)
13 |
14 | actual = fmt(Object.new, :red)
15 | assert actual.start_with?("\e[31m")
16 | assert actual.end_with?("\e[0m")
17 | end
18 |
19 | def test_fmt_print
20 | assert_output("\e[3mHello, World!\e[0m") { fmt_print("Hello, World!", :italic) }
21 | assert_output("\e[32mhello\e[0m") { fmt_print(:hello, :green) }
22 |
23 | actual = fmt(Object.new, :orange)
24 | assert actual.start_with?("\e[38;5;214m")
25 | assert actual.end_with?("\e[0m")
26 | end
27 |
28 | def test_fmt_puts
29 | assert_output("\e[1m\e[4mHello, World!\e[0m\n") { fmt_puts("Hello, World!", :bold, :underline) }
30 | assert_output("\e[35mhello\e[0m\n") { fmt_puts(:hello, :magenta) }
31 |
32 | actual = fmt(Object.new, :yellow)
33 | assert actual.start_with?("\e[33m")
34 | assert actual.end_with?("\e[0m")
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/test/test_fmt.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | require "test_helper"
6 |
7 | module Fmt
8 | class TestFmt < UnitTest
9 | def test_pipeline_simple
10 | assert_equal "Test", Fmt("%s", "Test")
11 | end
12 |
13 | def test_pipeline_multiple_compact
14 | assert_equal "abc", Fmt("%s%s%s", "a", "b", "c")
15 | end
16 |
17 | def test_pipeline_multiple_compact_named
18 | assert_equal "abc", Fmt("%{a}%{b}%{c}", a: "a", b: "b", c: "c")
19 | end
20 |
21 | def test_pipeline_multiple_compact_named_alt
22 | assert_equal "abc", Fmt("%s%s%s", a: "a", b: "b", c: "c")
23 | end
24 |
25 | def test_pipeline
26 | string = "%s|>indent(4)|>ljust(32, '.')|>cyan|>bold"
27 | assert_equal "\e[36m\e[1m Test........................\e[0m", Fmt(string, "Test")
28 | end
29 |
30 | def test_pipeline_named
31 | string = "%{value}|>indent(4)|>ljust(32, '.')|>cyan|>bold"
32 | assert_equal "\e[36m\e[1m Test........................\e[0m", Fmt(string, value: "Test")
33 | end
34 |
35 | def test_pipeline_named_alt
36 | string = "%s|>indent(4)|>ljust(32, '.')|>cyan|>bold"
37 | assert_equal "\e[36m\e[1m Test........................\e[0m", Fmt(string, value: "Test")
38 | end
39 |
40 | def test_pipelines
41 | string = "%s|>red %s|>blue %s|>green"
42 | assert_equal "\e[31mRed\e[0m \e[34mBlue\e[0m \e[32mGreen\e[0m", Fmt(string, "Red", "Blue", "Green")
43 | end
44 |
45 | def test_pipelines_named
46 | string = "%{a}|>red %{b}|>blue %{c}|>green"
47 | assert_equal "\e[31mRed\e[0m \e[34mBlue\e[0m \e[32mGreen\e[0m", Fmt(string, a: "Red", b: "Blue", c: "Green")
48 | end
49 |
50 | def test_pipelines_named_alt
51 | string = "%s|>red %s|>blue %s|>green"
52 | assert_equal "\e[31mRed\e[0m \e[34mBlue\e[0m \e[32mGreen\e[0m", Fmt(string, a: "Red", b: "Blue", c: "Green")
53 | end
54 |
55 | def test_embed
56 | string = "%{outer}|>faint {{%{inner}|>bold}}"
57 | assert_equal "\e[2mHello\e[0m \e[1mWorld!\e[0m", Fmt(string, outer: "Hello", inner: "World!")
58 | end
59 |
60 | def test_embed_with_pipeline
61 | string = "%{outer}|>faint {{%{inner}|>bold}}|>underline"
62 | assert_equal "\e[2mHello\e[0m \e[1m\e[4mWorld!\e[0m", Fmt(string, outer: "Hello", inner: "World!")
63 | end
64 |
65 | def test_embeds
66 | string = "%{a}|>faint {{%{b}|>bold {{%{c}|>red}}}}"
67 | assert_equal "\e[2mHello\e[0m \e[1mWorld\e[0m \e[31m!!!\e[0m", Fmt(string, a: "Hello", b: "World", c: "!!!")
68 | end
69 |
70 | def test_embeds_multiline
71 | string = <<~S
72 | %{a}|>red {{
73 | %{b}|>blue {{
74 | %{c}|>green
75 | }}|>bold
76 | }}
77 | S
78 | expected = "\e[31mRed\e[0m \n \e[34mBlue\e[0m \e[1m\n \e[32mGreen\e[0m\n \e[0m\n\n"
79 | actual = Fmt(string, a: "Red", b: "Blue", c: "Green")
80 | assert_equal expected, actual
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | require "bundler/setup"
6 |
7 | require "active_support/all"
8 | require "amazing_print"
9 | require "minitest/autorun"
10 | require "minitest/cc"
11 | require "minitest/reporters"
12 | require "pry-byebug"
13 | require "pry-doc"
14 | require "rainbow"
15 |
16 | Rainbow.enabled = true
17 |
18 | GC.disable
19 | AmazingPrint.defaults = {indent: 2, index: false, ruby19_syntax: true}
20 | AmazingPrint.pry!
21 | Minitest::Cc.start
22 | FileUtils.mkdir_p "tmp"
23 |
24 | Minitest::Reporters.use! [
25 | Minitest::Reporters::DefaultReporter.new(color: true, fail_fast: true, location: true),
26 | # Minitest::Reporters::SpecReporter.new(color: true, fail_fast: true, location: true),
27 | Minitest::Reporters::MeanTimeReporter.new(show_count: 5, show_progress: false, sort_column: :avg, previous_runs_filename: "tmp/minitest-report")
28 | ]
29 |
30 | require_relative "../lib/fmt"
31 |
32 | BENCHMARKS = {}
33 | TESTS = []
34 |
35 | Minitest.after_run do
36 | if ENV["BM"]
37 | BENCHMARKS.keys.sort.reverse_each do |key|
38 | BENCHMARKS[key].each do |value|
39 | puts case key
40 | in 5.. then Rainbow(value).crimson + Rainbow(" (#{key}ms)").crimson.bold
41 | in 1..5 then Rainbow(value).magenta + Rainbow(" (#{key}ms)").magenta.bold
42 | else Rainbow(value).cyan + Rainbow(" (#{key}ms)").cyan.bold
43 | end
44 | end
45 | end
46 |
47 | times = BENCHMARKS.keys[1..] # first test is slower due to resource loading (skip for avgeraging)
48 | average = (times.sum / times.size).round(2)
49 | print Rainbow("Average per/test ").gold
50 | puts Rainbow("(#{average}ms)").gold.bold
51 |
52 | end
53 |
54 | exit (TESTS.any? { _1.failures.any? }) ? 1 : 0
55 | end
56 |
57 | module Fmt
58 | class UnitTest < Minitest::Test
59 | def before_setup
60 | if ENV["BM"]
61 | @benchmark_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
62 | end
63 | end
64 |
65 | def after_teardown
66 | if ENV["BM"]
67 | return unless @benchmark_start_time
68 |
69 | end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
70 | duration_ms = ((end_time - @benchmark_start_time) * 1000).round(2)
71 |
72 | BENCHMARKS[duration_ms] ||= []
73 | BENCHMARKS[duration_ms] << "#{BENCHMARKS.values.flatten.size + 1}) #{self.class}##{name}"
74 |
75 | TESTS << self
76 | end
77 | end
78 |
79 | # Builds a Renderer for a string
80 | # @rbs string: String -- string to build the renderer for
81 | # @rbs return: Renderer
82 | def build_renderer(string)
83 | ast = TemplateParser.new(string).parse
84 | template = Template.new(ast)
85 | Renderer.new template
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/test/test_lru_cache.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | require_relative "test_helper"
6 |
7 | module Fmt
8 | class TestLRUCache < UnitTest
9 | def before_setup
10 | @cache = Fmt::LRUCache.new
11 | end
12 |
13 | def test_capacity
14 | @cache.capacity = 10
15 |
16 | 20.times { |i| @cache[:"key_#{i}"] = i }
17 | assert_equal 10, @cache.size
18 |
19 | @cache.clear
20 | assert_equal 0, @cache.size
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/test_native_formatters.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | require "test_helper"
6 |
7 | module Fmt
8 | class TestNativeFormatters < UnitTest
9 | def test_string
10 | assert_equal "Test", Fmt("%s", "Test")
11 | end
12 |
13 | def test_integer
14 | assert_equal "100", Fmt("%d", 100)
15 | end
16 |
17 | def test_binary
18 | assert_equal "100", Fmt("%b", 4)
19 | end
20 |
21 | def test_character
22 | assert_equal "A", Fmt("%c", 65)
23 | end
24 |
25 | def test_octal
26 | assert_equal "20", Fmt("%o", 16)
27 | end
28 |
29 | def test_hex
30 | assert_equal "64", Fmt("%x", 100)
31 | end
32 |
33 | def test_float
34 | assert_equal "3.141590", Fmt("%f", 3.14159)
35 | end
36 |
37 | def test_scientific
38 | assert_equal "3.141590e+00", Fmt("%e", 3.14159)
39 | end
40 |
41 | def test_inspect
42 | t = Struct.new(:a, :b, :c).new(:foo, "bar", true)
43 | assert_equal t.inspect, Fmt("%p", t)
44 | end
45 |
46 | def test_percent
47 | assert_equal "100%", Fmt("%d%%", 100)
48 | end
49 |
50 | def test_string_width
51 | assert_equal " Test", Fmt("%6s", "Test")
52 | assert_equal "Test ", Fmt("%-6s", "Test")
53 | end
54 |
55 | def test_integer_width_and_padding
56 | assert_equal " 100", Fmt("%5d", 100)
57 | assert_equal "00100", Fmt("%05d", 100)
58 | end
59 |
60 | def test_binary_width_and_padding
61 | assert_equal " 100", Fmt("%5b", 4)
62 | assert_equal "00100", Fmt("%05b", 4)
63 | end
64 |
65 | def test_octal_width_and_padding
66 | assert_equal " 20", Fmt("%4o", 16)
67 | assert_equal "0020", Fmt("%04o", 16)
68 | end
69 |
70 | def test_hex_width_and_padding
71 | assert_equal " 64", Fmt("%4x", 100)
72 | assert_equal "0064", Fmt("%04x", 100)
73 | assert_equal " 64", Fmt("%4X", 100)
74 | assert_equal "0064", Fmt("%04X", 100)
75 | end
76 |
77 | def test_float_precision
78 | assert_equal "3.14", Fmt("%.2f", 3.14159)
79 | assert_equal "3.1416", Fmt("%.4f", 3.14159)
80 | end
81 |
82 | def test_scientific_precision
83 | assert_equal "3.14e+00", Fmt("%.2e", 3.14159)
84 | assert_equal "3.1416E+00", Fmt("%.4E", 3.14159)
85 | end
86 |
87 | def test_inspect_limit
88 | long_string = "a" * 100
89 | assert_equal "\"aaaaaaaaaaaaaaaaaaaaaa", Fmt("%.23p", long_string)
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/test/test_rainbow_formatters.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # rbs_inline: enabled
4 |
5 | require "test_helper"
6 |
7 | module Fmt
8 | class TestRainbowFormatters < UnitTest
9 | def test_black
10 | assert_equal "\e[30mTest\e[0m", Fmt("%s|>black", "Test")
11 | end
12 |
13 | def test_red
14 | assert_equal "\e[31mTest\e[0m", Fmt("%s|>red", "Test")
15 | end
16 |
17 | def test_green
18 | assert_equal "\e[32mTest\e[0m", Fmt("%s|>green", "Test")
19 | end
20 |
21 | def test_yellow
22 | assert_equal "\e[33mTest\e[0m", Fmt("%s|>yellow", "Test")
23 | end
24 |
25 | def test_blue
26 | assert_equal "\e[34mTest\e[0m", Fmt("%s|>blue", "Test")
27 | end
28 |
29 | def test_magenta
30 | assert_equal "\e[35mTest\e[0m", Fmt("%s|>magenta", "Test")
31 | end
32 |
33 | def test_cyan
34 | assert_equal "\e[36mTest\e[0m", Fmt("%s|>cyan", "Test")
35 | end
36 |
37 | def test_white
38 | assert_equal "\e[37mTest\e[0m", Fmt("%s|>white", "Test")
39 | end
40 |
41 | def test_bg_black
42 | assert_equal "\e[40mTest\e[0m", Fmt("%s|>bg(:black)", "Test")
43 | end
44 |
45 | def test_bg_yellow
46 | assert_equal "\e[43mTest\e[0m", Fmt("%s|>bg(:yellow)", "Test")
47 | end
48 |
49 | def test_bright
50 | assert_equal "\e[1mTest\e[0m", Fmt("%s|>bright", "Test")
51 | end
52 |
53 | def test_underline
54 | assert_equal "\e[4mTest\e[0m", Fmt("%s|>underline", "Test")
55 | end
56 |
57 | def test_blink
58 | assert_equal "\e[5mTest\e[0m", Fmt("%s|>blink", "Test")
59 | end
60 |
61 | def test_inverse
62 | assert_equal "\e[7mTest\e[0m", Fmt("%s|>inverse", "Test")
63 | end
64 |
65 | def test_hide
66 | assert_equal "\e[8mTest\e[0m", Fmt("%s|>hide", "Test")
67 | end
68 |
69 | def test_faint
70 | assert_equal "\e[2mTest\e[0m", Fmt("%s|>faint", "Test")
71 | end
72 |
73 | def test_italic
74 | assert_equal "\e[3mTest\e[0m", Fmt("%s|>italic", "Test")
75 | end
76 |
77 | def test_cross_out
78 | assert_equal "\e[9mTest\e[0m", Fmt("%s|>cross_out", "Test")
79 | end
80 |
81 | def test_rgb_color
82 | assert_equal "\e[38;5;214mTest\e[0m", Fmt("%s|>color(255,128,0)", "Test")
83 | end
84 |
85 | def test_hex_color
86 | assert_equal "\e[38;5;214mTest\e[0m", Fmt("%s|>color('#FF8000')", "Test")
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/test/test_readme.rb:
--------------------------------------------------------------------------------
1 | # rbs_inline: enabled
2 |
3 | require "test_helper"
4 |
5 | module Fmt
6 | class TestReadme < UnitTest
7 | def test_e798c3
8 | assert_equal "Hello world!", Fmt("%s|>capitalize", "hello world!")
9 | assert_equal "Hello world!", Fmt("%{msg}|>capitalize", msg: "hello world!")
10 | end
11 |
12 | def test_1707d2
13 | assert_equal "Hello world!", Fmt("%s|>prepend('Hello ')", "world!")
14 | assert_equal "Hello world!", Fmt("%{msg}|>prepend('Hello ')", msg: "world!")
15 | end
16 |
17 | def test_425625
18 | expected = "HELLO WORLD!...................."
19 | assert_equal expected, Fmt("%s|>prepend('Hello ')|>ljust(32, '.')|>upcase", "world!")
20 | assert_equal expected, Fmt("%{msg}|>prepend('Hello ')|>ljust(32, '.')|>upcase", msg: "world!")
21 | end
22 |
23 | def test_f55ae2
24 | obj = Object.new
25 | expected = obj.inspect.partition(":").last.delete_suffix(">")
26 | actual = Fmt("%p|>partition(/:/)|>last|>delete_suffix('>')", obj)
27 | assert_equal expected, actual
28 | end
29 |
30 | def test_19c8ca
31 | expected = "\e[36m\e[1m\e[4mHello World!\e[0m"
32 | actual = Fmt("%s|>cyan|>bold|>underline", "Hello World!")
33 | assert_equal expected, actual
34 | end
35 |
36 | def test_0dbfcd
37 | time = Time.new(2024, 9, 21)
38 | template = "Date: %.10s|>magenta -- %{msg}|>titleize|>bold"
39 | expected = "Date: \e[35m2024-09-21\e[0m -- \e[1mThis Is Cool\e[0m"
40 | actual = Fmt(template, date: time, msg: "this is cool")
41 | assert_equal expected, actual
42 | end
43 |
44 | def test_efee7a
45 | template = "%{msg}|>faint {{%{embed}|>bold}}"
46 | expected = "\e[2mLook Ma...\e[0m \e[1mI'm embedded!\e[0m"
47 | actual = Fmt(template, msg: "Look Ma...", embed: "I'm embedded!")
48 | assert_equal expected, actual
49 | end
50 |
51 | def test_abb7ea
52 | template = "%{msg}|>faint {{%{embed}|>bold}}|>underline"
53 | expected = "\e[2mLook Ma...\e[0m \e[1m\e[4mI'm embedded!\e[0m"
54 | actual = Fmt(template, msg: "Look Ma...", embed: "I'm embedded!")
55 | assert_equal expected, actual
56 | end
57 |
58 | def test_79e924
59 | template = "%{msg}|>faint {{%{embed}|>bold {{%{deep_embed}|>red|>bold}}}}"
60 | expected = "\e[2mLook Ma...\e[0m \e[1mI'm embedded!\e[0m \e[31m\e[1mAnd I'm deeply embedded!\e[0m"
61 | actual = Fmt(template, msg: "Look Ma...", embed: "I'm embedded!", deep_embed: "And I'm deeply embedded!")
62 | assert_equal expected, actual
63 | end
64 |
65 | def test_054526
66 | template = <<~T
67 | Multiline:
68 | %{one}|>red {{
69 | %{two}|>blue {{
70 | %{three}|>green
71 | }}|>bold
72 | }}
73 | T
74 |
75 | expected = "Multiline:\n\e[31mRed\e[0m \n \e[34mBlue\e[0m \e[1m\n \e[32mGreen\e[0m\n \e[0m\n\n"
76 | actual = Fmt(template, one: "Red", two: "Blue", three: "Green")
77 | assert_equal expected, actual
78 | end
79 |
80 | def test_2cacce
81 | Fmt.register([Object, :shuffle]) { |*args, **kwargs| to_s.chars.shuffle.join }
82 | message = "This don't make no sense."
83 | refute_equal message, Fmt("%s|>shuffle", message)
84 | end
85 |
86 | def test_7df4eb
87 | Fmt.with_overrides([String, :red] => proc { |*args, **kwargs| Rainbow(self).crimson.bold }) do
88 | expected = "\e[38;5;197m\e[1mThis is customized red!\e[0m"
89 | actual = Fmt("%s|>red", "This is customized red!")
90 | assert_equal expected, actual
91 | end
92 |
93 | expected = "\e[31mThis is original red!\e[0m"
94 | actual = Fmt("%s|>red", "This is original red!")
95 | assert_equal expected, actual
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------