fmt 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +170 -87
- data/lib/fmt/boot.rb +50 -0
- data/lib/fmt/lru_cache.rb +181 -0
- data/lib/fmt/mixins/matchable.rb +26 -0
- data/lib/fmt/models/arguments.rb +194 -0
- data/lib/fmt/models/embed.rb +48 -0
- data/lib/fmt/models/macro.rb +58 -0
- data/lib/fmt/models/model.rb +66 -0
- data/lib/fmt/models/pipeline.rb +47 -0
- data/lib/fmt/models/template.rb +55 -0
- data/lib/fmt/node.rb +128 -0
- data/lib/fmt/parsers/arguments_parser.rb +43 -0
- data/lib/fmt/parsers/embed_parser.rb +54 -0
- data/lib/fmt/parsers/macro_parser.rb +113 -0
- data/lib/fmt/parsers/parser.rb +56 -0
- data/lib/fmt/parsers/pipeline_parser.rb +41 -0
- data/lib/fmt/parsers/template_parser.rb +125 -0
- data/lib/fmt/refinements/kernel_refinement.rb +38 -0
- data/lib/fmt/registries/native_registry.rb +66 -0
- data/lib/fmt/registries/rainbow_registry.rb +36 -0
- data/lib/fmt/registries/registry.rb +127 -0
- data/lib/fmt/renderer.rb +132 -0
- data/lib/fmt/sigils.rb +23 -0
- data/lib/fmt/token.rb +126 -0
- data/lib/fmt/tokenizer.rb +96 -0
- data/lib/fmt/version.rb +3 -1
- data/lib/fmt.rb +51 -11
- data/sig/generated/fmt/boot.rbs +2 -0
- data/sig/generated/fmt/lru_cache.rbs +122 -0
- data/sig/generated/fmt/mixins/matchable.rbs +18 -0
- data/sig/generated/fmt/models/arguments.rbs +115 -0
- data/sig/generated/fmt/models/embed.rbs +34 -0
- data/sig/generated/fmt/models/macro.rbs +37 -0
- data/sig/generated/fmt/models/model.rbs +45 -0
- data/sig/generated/fmt/models/pipeline.rbs +31 -0
- data/sig/generated/fmt/models/template.rbs +33 -0
- data/sig/generated/fmt/node.rbs +64 -0
- data/sig/generated/fmt/parsers/arguments_parser.rbs +25 -0
- data/sig/generated/fmt/parsers/embed_parser.rbs +36 -0
- data/sig/generated/fmt/parsers/macro_parser.rbs +60 -0
- data/sig/generated/fmt/parsers/parser.rbs +44 -0
- data/sig/generated/fmt/parsers/pipeline_parser.rbs +25 -0
- data/sig/generated/fmt/parsers/template_parser.rbs +50 -0
- data/sig/generated/fmt/refinements/kernel_refinement.rbs +23 -0
- data/sig/generated/fmt/registries/native_registry.rbs +19 -0
- data/sig/generated/fmt/registries/rainbow_registry.rbs +11 -0
- data/sig/generated/fmt/registries/registry.rbs +69 -0
- data/sig/generated/fmt/renderer.rbs +70 -0
- data/sig/generated/fmt/sigils.rbs +30 -0
- data/sig/generated/fmt/token.rbs +77 -0
- data/sig/generated/fmt/tokenizer.rbs +51 -0
- data/sig/generated/fmt/version.rbs +5 -0
- data/sig/generated/fmt.rbs +41 -0
- metadata +126 -16
- data/lib/fmt/embed.rb +0 -19
- data/lib/fmt/filter.rb +0 -32
- data/lib/fmt/filters.rb +0 -76
- data/lib/fmt/formatter.rb +0 -50
- data/lib/fmt/scanners/base_scanner.rb +0 -41
- data/lib/fmt/scanners/embed_scanner.rb +0 -56
- data/lib/fmt/scanners/filter_scanner.rb +0 -31
- data/lib/fmt/scanners/key_scanner.rb +0 -15
- data/lib/fmt/scanners.rb +0 -3
- data/lib/fmt/transformer.rb +0 -63
@@ -0,0 +1,113 @@
|
|
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
|
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
|
28
|
+
code = "#{Sigils::FORMAT_METHOD}('#{urtext}')" if native_format_string?(urtext)
|
29
|
+
|
30
|
+
tokens = tokenize(code)
|
31
|
+
method = tokens.find(&:method_name?)&.value&.to_sym
|
32
|
+
|
33
|
+
arguments_tokens = case arguments?(tokens)
|
34
|
+
in false then []
|
35
|
+
else
|
36
|
+
arguments_start = tokens.index(tokens.find(&:arguments_start?)).to_i
|
37
|
+
arguments_finish = tokens.index(tokens.find(&:arguments_finish?)).to_i
|
38
|
+
tokens[arguments_start..arguments_finish]
|
39
|
+
end
|
40
|
+
|
41
|
+
{method: method, arguments_tokens: arguments_tokens}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Transforms extracted components into an AST (Abstract Syntax Tree)
|
45
|
+
# @rbs method: Symbol?
|
46
|
+
# @rbs arguments_tokens: Array[Token] -- arguments tokens
|
47
|
+
# @rbs return: Node -- AST (Abstract Syntax Tree)
|
48
|
+
def transform(method:, arguments_tokens:)
|
49
|
+
method = Node.new(:name, [method], urtext: urtext, source: method)
|
50
|
+
arguments = ArgumentsParser.new(arguments_tokens).parse
|
51
|
+
source = "#{method.source}#{arguments.source}"
|
52
|
+
children = [method, arguments].reject(&:empty?)
|
53
|
+
|
54
|
+
Node.new :macro, children.reject(&:empty?), urtext: urtext, source: source
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Tokenizes source code
|
60
|
+
# @rbs code: String -- source code to tokenize
|
61
|
+
# @rbs return: Array[Token] -- wrapped ripper tokens
|
62
|
+
def tokenize(code)
|
63
|
+
tokens = Tokenizer.new(code).tokenize
|
64
|
+
macro = []
|
65
|
+
|
66
|
+
tokens.each do |token|
|
67
|
+
break if token.whitespace? && macro_finished?(macro)
|
68
|
+
macro << token
|
69
|
+
end
|
70
|
+
|
71
|
+
macro
|
72
|
+
end
|
73
|
+
|
74
|
+
# Indicates if there is a set of arguments in the tokens
|
75
|
+
# @rbs tokens: Array[Token] -- tokens to check
|
76
|
+
# @rbs return: bool
|
77
|
+
def arguments?(tokens)
|
78
|
+
arguments_started?(tokens) && arguments_finished?(tokens)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Indicates if arguments have started
|
82
|
+
# @rbs tokens: Array[Token] -- tokens to check
|
83
|
+
# @rbs return: bool
|
84
|
+
def arguments_started?(tokens)
|
85
|
+
tokens.any? { _1.arguments_start? }
|
86
|
+
end
|
87
|
+
|
88
|
+
# Indicates if arguments have finished
|
89
|
+
# @note Call this after a whitespace has been detected
|
90
|
+
# @rbs tokens: Array[Token] -- tokens to check
|
91
|
+
# @rbs return: bool
|
92
|
+
def arguments_finished?(tokens)
|
93
|
+
tokens.any? { _1.arguments_finish? }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Indicates if a macro token array is complete or finished
|
97
|
+
# @note Call this after a whitespace has been detected
|
98
|
+
# @rbs tokens: Array[Token] -- tokens to check
|
99
|
+
# @rbs return: bool
|
100
|
+
def finished?(tokens)
|
101
|
+
return false unless tokens.any? { _1.method_name? }
|
102
|
+
return false if arguments_started?(tokens) && !arguments_finished?(tokens)
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
# Indicates if a value is a Ruby native format string
|
107
|
+
# @rbs value: String -- value to check
|
108
|
+
# @rbs return: bool
|
109
|
+
def native_format_string?(value)
|
110
|
+
value.start_with? Sigils::FORMAT_PREFIX
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,56 @@
|
|
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
|
@@ -0,0 +1,41 @@
|
|
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
|
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
|
@@ -0,0 +1,125 @@
|
|
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
|
+
PIPELINE_HEAD = %r{(?=(?!#{esc Sigils::PIPE_OPERATOR})#{Sigils::FORMAT_PREFIX})}o # : Regexp -- detects a native Ruby format string
|
9
|
+
PIPELINE_TAIL = %r{(?=(\s+#{Sigils::FORMAT_PREFIX})|\z)}o # : Regexp -- detects a pipeline suffix
|
10
|
+
|
11
|
+
EMBED_HEAD = %r{(?=#{esc Sigils::EMBED_PREFIX})}o # : Regexp -- detects an embed prefix
|
12
|
+
EMBED_TAIL = %r{#{esc Sigils::EMBED_SUFFIX}}o # : Regexp -- detects an embed suffix
|
13
|
+
|
14
|
+
# Constructor
|
15
|
+
# @rbs urtext: String -- original source code
|
16
|
+
# @rbs scanner: StringScanner?
|
17
|
+
def initialize(urtext = "", scanner: nil)
|
18
|
+
@urtext = urtext.to_s
|
19
|
+
@scanner = scanner || StringScanner.new(@urtext)
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :urtext # : String -- original source code
|
23
|
+
attr_reader :scanner # : StringScanner?
|
24
|
+
|
25
|
+
# Parses the urtext (original source code)
|
26
|
+
# @rbs return: Node -- AST (Abstract Syntax Tree)
|
27
|
+
def parse
|
28
|
+
cache(urtext) { super }
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
# Extracts components for building the AST (Abstract Syntax Tree)
|
34
|
+
# @note Extraction is delegated to the PipelineParser and EmbedParser in transform
|
35
|
+
# @rbs return: Hash
|
36
|
+
def extract
|
37
|
+
source = urtext
|
38
|
+
|
39
|
+
embeds = extract_embeds
|
40
|
+
embeds.each do |embed|
|
41
|
+
source = "#{source[0...embed[:index]]}#{embed[:placeholder]}#{source[embed[:rindex]..]}"
|
42
|
+
end
|
43
|
+
|
44
|
+
pipelines = extract_pipelines(source)
|
45
|
+
|
46
|
+
{embeds: embeds, pipelines: pipelines, source: source}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Transforms extracted components into an AST (Abstract Syntax Tree)
|
50
|
+
# @rbs embeds: Array[Hash] -- extracted embeds
|
51
|
+
# @rbs pipelines: Array[String] -- extracted pipelines
|
52
|
+
# @rbs source: String -- parsed source code
|
53
|
+
# @rbs return: Node -- AST (Abstract Syntax Tree)
|
54
|
+
def transform(embeds:, pipelines:, source:)
|
55
|
+
embeds = embeds.map { EmbedParser.new(_1[:urtext], **_1.slice(:key, :placeholder)).parse }
|
56
|
+
embeds = Node.new(:embeds, embeds, urtext: urtext, source: urtext)
|
57
|
+
|
58
|
+
pipelines = pipelines.map { PipelineParser.new(_1).parse }
|
59
|
+
pipelines = Node.new(:pipelines, pipelines, urtext: urtext, source: source)
|
60
|
+
|
61
|
+
children = [embeds, pipelines].reject(&:empty?)
|
62
|
+
|
63
|
+
Node.new :template, children, urtext: urtext, source: source
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Extracts embed metadata from the urtext
|
69
|
+
# @rbs return: Array[Hash] -- extracted embeds
|
70
|
+
def extract_embeds
|
71
|
+
embeds = []
|
72
|
+
|
73
|
+
index = nil
|
74
|
+
embed = ""
|
75
|
+
|
76
|
+
scanner = StringScanner.new(urtext)
|
77
|
+
scanner.skip_until(EMBED_HEAD)
|
78
|
+
|
79
|
+
while scanner.matched?
|
80
|
+
index ||= scanner.charpos
|
81
|
+
embed = "#{embed}#{scanner.scan_until(EMBED_TAIL)}"
|
82
|
+
|
83
|
+
if embed.scan(EMBED_HEAD).size == embed.scan(EMBED_TAIL).size
|
84
|
+
rindex = scanner.charpos
|
85
|
+
key = :"embed_#{index}_#{rindex}"
|
86
|
+
|
87
|
+
embeds << {
|
88
|
+
index: index,
|
89
|
+
rindex: rindex,
|
90
|
+
key: key,
|
91
|
+
placeholder: "#{Sigils::FORMAT_PREFIX}#{Sigils::KEY_PREFIXES[-1]}#{key}#{Sigils::KEY_SUFFIXES[-1]}",
|
92
|
+
urtext: embed
|
93
|
+
}
|
94
|
+
|
95
|
+
index = nil
|
96
|
+
embed = scanner.skip_until(EMBED_HEAD)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
embeds
|
101
|
+
end
|
102
|
+
|
103
|
+
# Extracts pipelines from the source
|
104
|
+
# @rbs source: String -- source code to extract pipelines from
|
105
|
+
# @rbs return: Array[String] -- extracted pipelines
|
106
|
+
def extract_pipelines(source)
|
107
|
+
pipelines = []
|
108
|
+
pipeline = ""
|
109
|
+
|
110
|
+
scanner = StringScanner.new(source)
|
111
|
+
scanner.skip_until(PIPELINE_HEAD)
|
112
|
+
|
113
|
+
while scanner.matched?
|
114
|
+
pipeline = scanner.scan_until(PIPELINE_TAIL)
|
115
|
+
|
116
|
+
if scanner.matched?
|
117
|
+
pipelines << pipeline
|
118
|
+
scanner.skip_until(PIPELINE_HEAD)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
pipelines
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,38 @@
|
|
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
|
+
text = case object
|
14
|
+
in String then object
|
15
|
+
in Symbol then object.to_s
|
16
|
+
else object.inspect
|
17
|
+
end
|
18
|
+
Fmt "%s|>to_s|>#{pipeline.join("|>")}", text
|
19
|
+
end
|
20
|
+
|
21
|
+
# Formats an object with Fmt and prints to STDOUT
|
22
|
+
# @rbs object [Object] -- object to format (coerced to String)
|
23
|
+
# @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
|
24
|
+
# @rbs return void
|
25
|
+
def fmt_print(object, *pipeline)
|
26
|
+
puts fmt(object, *pipeline)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Formats an object with Fmt and puts to STDOUT
|
30
|
+
# @rbs object [Object] -- object to format (coerced to String)
|
31
|
+
# @rbs pipeline [Array[String | Symbol]] -- Fmt pipeline
|
32
|
+
# @rbs return void
|
33
|
+
def fmt_puts(object, *pipeline)
|
34
|
+
puts fmt(object, *pipeline)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,66 @@
|
|
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
|
@@ -0,0 +1,36 @@
|
|
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
|
@@ -0,0 +1,127 @@
|
|
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
|