fmt 0.1.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +167 -93
- 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 +50 -12
- 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 -18
- data/lib/fmt/embed.rb +0 -19
- data/lib/fmt/filter.rb +0 -32
- data/lib/fmt/filter_groups/filter_group.rb +0 -56
- data/lib/fmt/filter_groups/rainbow_filter_group.rb +0 -27
- data/lib/fmt/filter_groups/string_filter_group.rb +0 -28
- data/lib/fmt/formatter.rb +0 -60
- 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 -57
@@ -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
|