mustermann-visualizer 0.4.0

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