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.
@@ -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