mustermann-visualizer 0.4.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 +7 -0
- data/README.md +196 -0
- data/examples/highlighting.rb +27 -0
- data/highlighting.png +0 -0
- data/irb.png +0 -0
- data/lib/mustermann/visualizer.rb +38 -0
- data/lib/mustermann/visualizer/highlight.rb +136 -0
- data/lib/mustermann/visualizer/highlighter.rb +36 -0
- data/lib/mustermann/visualizer/highlighter/ad_hoc.rb +94 -0
- data/lib/mustermann/visualizer/highlighter/ast.rb +102 -0
- data/lib/mustermann/visualizer/highlighter/dummy.rb +18 -0
- data/lib/mustermann/visualizer/highlighter/regular.rb +104 -0
- data/lib/mustermann/visualizer/pattern_extension.rb +66 -0
- data/lib/mustermann/visualizer/renderer/ansi.rb +23 -0
- data/lib/mustermann/visualizer/renderer/generic.rb +49 -0
- data/lib/mustermann/visualizer/renderer/hansi_template.rb +34 -0
- data/lib/mustermann/visualizer/renderer/html.rb +50 -0
- data/lib/mustermann/visualizer/renderer/sexp.rb +37 -0
- data/lib/mustermann/visualizer/tree.rb +63 -0
- data/lib/mustermann/visualizer/tree_renderer.rb +78 -0
- data/mustermann-visualizer.gemspec +19 -0
- data/spec/pattern_extension_spec.rb +48 -0
- data/spec/visualizer_spec.rb +179 -0
- data/theme.png +0 -0
- data/tree.png +0 -0
- metadata +98 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module Visualizer
|
5
|
+
# @!visibility private
|
6
|
+
module Highlighter
|
7
|
+
# Used to generate highlighting rules on the fly.
|
8
|
+
# @see {Mustermann::Shell#highlighter}
|
9
|
+
# @see {Mustermann::Simple#highlighter}
|
10
|
+
# @!visibility private
|
11
|
+
class AdHoc
|
12
|
+
# @!visibility private
|
13
|
+
def self.highlight(pattern, renderer)
|
14
|
+
new(pattern, renderer).highlight
|
15
|
+
end
|
16
|
+
|
17
|
+
# @!visibility private
|
18
|
+
def self.rules
|
19
|
+
@rules ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# @!visibility private
|
23
|
+
def self.on(regexp, type = nil, &callback)
|
24
|
+
return regexp.map { |key, value| on(key, value, &callback) } if regexp.is_a? Hash
|
25
|
+
raise ArgumentError, 'needs type or callback' unless type or callback
|
26
|
+
callback ||= proc { |matched| element(type, matched) }
|
27
|
+
regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp
|
28
|
+
rules[regexp] = callback
|
29
|
+
end
|
30
|
+
|
31
|
+
# @!visibility private
|
32
|
+
attr_reader :pattern, :renderer, :rules, :output, :scanner
|
33
|
+
def initialize(pattern, renderer)
|
34
|
+
@pattern = pattern
|
35
|
+
@renderer = renderer
|
36
|
+
@output = ""
|
37
|
+
@rules = self.class.rules
|
38
|
+
@scanner = StringScanner.new(pattern.to_s)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @!visibility private
|
42
|
+
def highlight(stop = /\Z/)
|
43
|
+
output << renderer.pre(:root)
|
44
|
+
until scanner.eos? or scanner.check(stop)
|
45
|
+
position = scanner.pos
|
46
|
+
apply(scanner)
|
47
|
+
read_char(scanner) if position == scanner.pos and not scanner.check(stop)
|
48
|
+
end
|
49
|
+
output << renderer.post(:root)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @!visibility private
|
53
|
+
def apply(scanner)
|
54
|
+
rules.each do |regexp, callback|
|
55
|
+
next unless result = scanner.scan(regexp)
|
56
|
+
instance_exec(result, &callback)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @!visibility private
|
61
|
+
def read_char(scanner)
|
62
|
+
return unless char = scanner.getch
|
63
|
+
type = char == ?/ ? :separator : :char
|
64
|
+
element(type, char)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @!visibility private
|
68
|
+
def escaped(content = ?\\, char)
|
69
|
+
element(:escaped, content) { element(:escaped_char, char) }
|
70
|
+
end
|
71
|
+
|
72
|
+
# @!visibility private
|
73
|
+
def nested(type, opening, closing, *separators)
|
74
|
+
element(type, opening) do
|
75
|
+
char = nil
|
76
|
+
until char == closing or scanner.eos?
|
77
|
+
highlight(Regexp.union(closing, *separators))
|
78
|
+
char = scanner.getch
|
79
|
+
output << char if char
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# @!visibility private
|
85
|
+
def element(type, content = nil)
|
86
|
+
output << renderer.pre(type)
|
87
|
+
output << renderer.escape(content) if content
|
88
|
+
yield if block_given?
|
89
|
+
output << renderer.post(type)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'mustermann/ast/translator'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module Visualizer
|
5
|
+
# @!visibility private
|
6
|
+
module Highlighter
|
7
|
+
# Provides highlighting for AST based patterns
|
8
|
+
# @!visibility private
|
9
|
+
class AST
|
10
|
+
Index = Struct.new(:type, :start, :stop, :payload) { undef :to_a }
|
11
|
+
Indexer = Mustermann::AST::Translator.create do
|
12
|
+
translate(:node) { |i| Index.new(type, start, stop, Array(t(payload, i)).flatten.compact) }
|
13
|
+
translate(Array) { |i| map { |e| t(e, i) } }
|
14
|
+
translate(Object) { |i| }
|
15
|
+
|
16
|
+
translate(:with_look_ahead) do |input|
|
17
|
+
[t(head, input), *t(payload, input)]
|
18
|
+
end
|
19
|
+
|
20
|
+
translate(:expression) do |input|
|
21
|
+
index = Index.new(type, start, stop, Array(t(payload, input)).compact)
|
22
|
+
index.payload.delete_if { |e| e.type == :separator }
|
23
|
+
index
|
24
|
+
end
|
25
|
+
|
26
|
+
translate(:capture) do |input|
|
27
|
+
substring = input[start, length]
|
28
|
+
if substart = substring.index(name)
|
29
|
+
substart += start
|
30
|
+
substop = substart + name.length
|
31
|
+
payload = [Index.new(:name, substart, substop, [])]
|
32
|
+
end
|
33
|
+
Index.new(type, start, stop, payload || [])
|
34
|
+
end
|
35
|
+
|
36
|
+
translate(:char) do |input|
|
37
|
+
substring = input[start, length]
|
38
|
+
if payload == substring
|
39
|
+
Index.new(type, start, stop, [])
|
40
|
+
elsif substart = substring.index(payload)
|
41
|
+
substart += start
|
42
|
+
substop = substart + payload.length
|
43
|
+
Index.new(:escaped, start, stop, [Index.new(:escaped_char, substart, substop, [])])
|
44
|
+
else
|
45
|
+
Index.new(:escaped, start, stop, [])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private_constant(:Index, :Indexer)
|
51
|
+
|
52
|
+
# @!visibility private
|
53
|
+
def self.highlight?(pattern)
|
54
|
+
pattern.respond_to? :to_ast
|
55
|
+
end
|
56
|
+
|
57
|
+
# @!visibility private
|
58
|
+
def self.highlight(pattern, renderer)
|
59
|
+
new(pattern, renderer).highlight
|
60
|
+
end
|
61
|
+
|
62
|
+
# @!visibility private
|
63
|
+
def initialize(pattern, renderer)
|
64
|
+
@ast = pattern.to_ast
|
65
|
+
@string = pattern.to_s
|
66
|
+
@renderer = renderer
|
67
|
+
end
|
68
|
+
|
69
|
+
# @!visibility private
|
70
|
+
def highlight
|
71
|
+
index = Indexer.translate(@ast, @string)
|
72
|
+
inject_literals(index)
|
73
|
+
render(index)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @!visibility private
|
77
|
+
def render(index)
|
78
|
+
return @renderer.escape(@string[index.start..index.stop-1]) if index.type == :literal
|
79
|
+
payload = index.payload.map { |i| render(i) }.join
|
80
|
+
"#{ @renderer.pre(index.type) }#{ payload }#{ @renderer.post(index.type) }"
|
81
|
+
end
|
82
|
+
|
83
|
+
# @!visibility private
|
84
|
+
def inject_literals(index)
|
85
|
+
start, old_payload, index.payload = index.start, index.payload, []
|
86
|
+
old_payload.each do |element|
|
87
|
+
index.payload << literal(start, element.start) if start < element.start
|
88
|
+
index.payload << element
|
89
|
+
inject_literals(element)
|
90
|
+
start = element.stop
|
91
|
+
end
|
92
|
+
index.payload << literal(start, index.stop) if start < index.stop
|
93
|
+
end
|
94
|
+
|
95
|
+
# @!visibility private
|
96
|
+
def literal(start, stop)
|
97
|
+
Index.new(:literal, start, stop, [])
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Mustermann
|
2
|
+
module Visualizer
|
3
|
+
# @!visibility private
|
4
|
+
module Highlighter
|
5
|
+
# Provides highlighting for patterns that don't have a highlighter.
|
6
|
+
# @!visibility private
|
7
|
+
module Dummy
|
8
|
+
# @!visibility private
|
9
|
+
def self.highlight(pattern, renderer)
|
10
|
+
output = ""
|
11
|
+
output << renderer.pre(:root) << renderer.pre(:unknown)
|
12
|
+
output << renderer.escape(pattern.to_s)
|
13
|
+
output << renderer.post(:unknown) << renderer.post(:root)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module Visualizer
|
5
|
+
# @!visibility private
|
6
|
+
module Highlighter
|
7
|
+
# Provides highlighting for {Mustermann::Regular}
|
8
|
+
# @!visibility private
|
9
|
+
class Regular
|
10
|
+
# @!visibility private
|
11
|
+
SPECIAL_ESCAPE = ['w', 'W', 'd', 'D', 'h', 'H', 's', 'S', 'G', 'b', 'B']
|
12
|
+
private_constant(:SPECIAL_ESCAPE)
|
13
|
+
|
14
|
+
# @!visibility private
|
15
|
+
def self.highlight?(pattern)
|
16
|
+
pattern.class.name == "Mustermann::Regular"
|
17
|
+
end
|
18
|
+
|
19
|
+
# @!visibility private
|
20
|
+
def self.highlight(pattern, renderer)
|
21
|
+
new(renderer).highlight(pattern)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @!visibility private
|
25
|
+
attr_reader :renderer, :output, :scanner
|
26
|
+
|
27
|
+
# @!visibility private
|
28
|
+
def initialize(renderer)
|
29
|
+
@renderer = renderer
|
30
|
+
@output = ""
|
31
|
+
end
|
32
|
+
|
33
|
+
# @!visibility private
|
34
|
+
def highlight(pattern)
|
35
|
+
output << renderer.pre(:root)
|
36
|
+
@scanner = StringScanner.new(pattern.to_s)
|
37
|
+
scan
|
38
|
+
output << renderer.post(:root)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @!visibility private
|
42
|
+
def scan(stop = nil)
|
43
|
+
until scanner.eos?
|
44
|
+
case char = scanner.getch
|
45
|
+
when stop then return char
|
46
|
+
when ?/ then element(:separator, char)
|
47
|
+
when Regexp.escape(char) then element(:char, char)
|
48
|
+
when ?\\ then escaped(scanner.getch)
|
49
|
+
when ?( then potential_capture
|
50
|
+
when ?[ then char_class
|
51
|
+
when ?^, ?$ then element(:illegal, char)
|
52
|
+
when ?{ then element(:special, "\{#{scanner.scan(/[^\}]*\}/)}")
|
53
|
+
else element(:special, char)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# @!visibility private
|
59
|
+
def char_class
|
60
|
+
if result = scanner.scan(/\[:\w+:\]\]/)
|
61
|
+
element(:special, "[#{result}")
|
62
|
+
else
|
63
|
+
element(:special, ?[)
|
64
|
+
element(:special, ?^) if scanner.scan(/\^/)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @!visibility private
|
69
|
+
def potential_capture
|
70
|
+
if scanner.scan(/\?<(\w+)>/)
|
71
|
+
element(:capture, "(?<") do
|
72
|
+
element(:name, scanner[1])
|
73
|
+
output << ">" << scan(?))
|
74
|
+
end
|
75
|
+
elsif scanner.scan(/\?(?:(?:-\w+)?:|>|<=|<!|!|=)/)
|
76
|
+
element(:special, "(#{scanner[0]}")
|
77
|
+
else
|
78
|
+
element(:capture, "(") { output << scan(?)) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @!visibility private
|
83
|
+
def escaped(char)
|
84
|
+
case char
|
85
|
+
when *SPECIAL_ESCAPE then element(:special, "\\#{char}")
|
86
|
+
when 'A', 'Z', 'z' then element(:illegal, "\\#{char}")
|
87
|
+
when 'g' then element(:special, "\\#{char}#{scanner.scan(/<\w*>/)}")
|
88
|
+
when 'p', 'u' then element(:special, "\\#{char}#{scanner.scan(/\{[^\}]*\}/)}")
|
89
|
+
when ?/ then element(:separator, char)
|
90
|
+
else element(:escaped, ?\\) { element(:escaped_char, char) }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# @!visibility private
|
95
|
+
def element(type, content = nil)
|
96
|
+
output << renderer.pre(type)
|
97
|
+
output << renderer.escape(content) if content
|
98
|
+
yield if block_given?
|
99
|
+
output << renderer.post(type)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Mustermann
|
2
|
+
module Visualizer
|
3
|
+
# Mixin that will be added to {Mustermann::Pattern}.
|
4
|
+
module PatternExtension
|
5
|
+
prepend_features Pattern
|
6
|
+
|
7
|
+
# @example
|
8
|
+
# puts Mustermann.new("/:page").to_ansi
|
9
|
+
#
|
10
|
+
# @return [String] ANSI colorized version of the pattern.
|
11
|
+
def to_ansi(inspect: false, **theme)
|
12
|
+
Visualizer.highlight(self, **theme).to_ansi(inspect: inspect)
|
13
|
+
end
|
14
|
+
|
15
|
+
# @example
|
16
|
+
# puts Mustermann.new("/:page").to_html
|
17
|
+
#
|
18
|
+
# @return [String] HTML version of the pattern.
|
19
|
+
def to_html(inspect: false, tag: :span, class_prefix: "mustermann_", css: :inline, **theme)
|
20
|
+
Visualizer.highlight(self, **theme).to_html(inspect: inspect, tag: tag, class_prefix: class_prefix, css: css)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @example
|
24
|
+
# puts Mustermann.new("/:page").to_tree
|
25
|
+
#
|
26
|
+
# @return [String] tree version of the pattern.
|
27
|
+
def to_tree
|
28
|
+
Visualizer.tree(self).to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
# If invoked directly by puts: ANSI colorized version of the pattern.
|
32
|
+
# If invoked by anything else: String version of the pattern.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# require 'mustermann/visualizer'
|
36
|
+
# pattern = Mustermann.new('/:page')
|
37
|
+
# puts pattern # will have color
|
38
|
+
# puts pattern.to_s # will not have color
|
39
|
+
#
|
40
|
+
# @return [String] non-colorized or colorized version of the pattern
|
41
|
+
def to_s
|
42
|
+
caller_locations.first.label == 'puts' ? to_ansi : super
|
43
|
+
end
|
44
|
+
|
45
|
+
# If invoked directly by IRB, same as {#color_inspect}, otherwise same as {Mustermann::Pattern#inspect}.
|
46
|
+
def inspect
|
47
|
+
caller_locations.first.base_label == '<module:IRB>' ? color_inspect : super
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [String] ANSI colorized version of {Mustermann::Pattern#inspect}
|
51
|
+
def color_inspect(base_color = nil, **theme)
|
52
|
+
base_color ||= Highlight::DEFAULT_THEME[:base01]
|
53
|
+
Hansi.render("*#<%p:*%s*>*", self.class, to_ansi(inspect: true, **theme), "*" => base_color)
|
54
|
+
end
|
55
|
+
|
56
|
+
# If invoked directly by IRB, same as {#color_inspect}, otherwise same as Object#pretty_print.
|
57
|
+
def pretty_print(q)
|
58
|
+
if q.class.name.to_s[/[^:]+$/] == "ColorPrinter"
|
59
|
+
q.text(color_inspect, inspect.length)
|
60
|
+
else
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Mustermann
|
2
|
+
module Visualizer
|
3
|
+
# @!visibility private
|
4
|
+
module Renderer
|
5
|
+
# Generates ANSI colored strings.
|
6
|
+
# @!visibility private
|
7
|
+
class ANSI
|
8
|
+
# @!visibility private
|
9
|
+
def initialize(target, mode: Hansi.mode, **options)
|
10
|
+
@target = target
|
11
|
+
@mode = mode
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
# @!visibility private
|
16
|
+
def render
|
17
|
+
template = @target.to_hansi_template(**@options)
|
18
|
+
Hansi.render(template, tags: true, theme: @target.theme, mode: @mode)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'mustermann/regular'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module Visualizer
|
5
|
+
# @!visibility private
|
6
|
+
module Renderer
|
7
|
+
# Logic shared by most renderers.
|
8
|
+
class Generic
|
9
|
+
# @!visibility private
|
10
|
+
def initialize(target, inspect: false)
|
11
|
+
@target = target
|
12
|
+
@inspect = inspect
|
13
|
+
end
|
14
|
+
|
15
|
+
# @!visibility private
|
16
|
+
def render
|
17
|
+
quote = @inspect ? "#{pre(:quote)}\"#{post(:quote)}" : ""
|
18
|
+
pre(:pattern).to_s + preamble.to_s + quote + @target.render(self) + quote + post(:pattern).to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
# @!visibility private
|
22
|
+
def preamble
|
23
|
+
end
|
24
|
+
|
25
|
+
# @!visibility private
|
26
|
+
def escape(value)
|
27
|
+
value = value.to_s
|
28
|
+
value = value.inspect[1..-2] if @inspect
|
29
|
+
escape_string(value)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @!visibility private
|
33
|
+
def escape_string(string)
|
34
|
+
string
|
35
|
+
end
|
36
|
+
|
37
|
+
# @!visibility private
|
38
|
+
def pre(type)
|
39
|
+
""
|
40
|
+
end
|
41
|
+
|
42
|
+
# @!visibility private
|
43
|
+
def post(type)
|
44
|
+
""
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|