├── .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 | Lines of Code 4 | 5 | 6 | GEM Version 7 | 8 | 9 | GEM Downloads 10 | 11 | 12 | Tests 13 | 14 | 15 | Ruby Style 16 | 17 | 18 | Sponsors 19 | 20 | 21 | Twitter Follow 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 | --------------------------------------------------------------------------------