mustermann-visualizer 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|