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,34 @@
1
+ require 'mustermann/visualizer/renderer/generic'
2
+
3
+ module Mustermann
4
+ module Visualizer
5
+ # @!visibility private
6
+ module Renderer
7
+ # Generates Hansi template string.
8
+ # @see Mustermann::Visualizer::Renderer::ANSI
9
+ # @!visibility private
10
+ class HansiTemplate < Generic
11
+ # @!visibility private
12
+ def initialize(*)
13
+ @hansi = Hansi::StringRenderer.new(tags: true)
14
+ super
15
+ end
16
+
17
+ # @!visibility private
18
+ def escape_string(string)
19
+ @hansi.escape(string)
20
+ end
21
+
22
+ # @!visibility private
23
+ def pre(type)
24
+ "<#{type}>"
25
+ end
26
+
27
+ # @!visibility private
28
+ def post(type)
29
+ "</#{type}>"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,50 @@
1
+ require 'mustermann/visualizer/renderer/generic'
2
+ require 'cgi'
3
+
4
+ module Mustermann
5
+ module Visualizer
6
+ # @!visibility private
7
+ module Renderer
8
+ # Generates HTML output.
9
+ # @!visibility private
10
+ class HTML < Generic
11
+ # @!visibility private
12
+ def initialize(target, tag: :span, class_prefix: "mustermann_", css: :inline, **options)
13
+ raise ArgumentError, 'css option %p not supported, should be true, false or inline' if css != true and css != false and css != :inline
14
+ super(target, **options)
15
+ @css, @tag, @class_prefix = css, tag, class_prefix
16
+ end
17
+
18
+ # @!visibility private
19
+ def preamble
20
+ "<style type=\"text/css\">\n%s</style>" % stylesheet if @css == true
21
+ end
22
+
23
+ # @!visibility private
24
+ def stylesheet
25
+ @target.theme.to_css { |name| ".#{@class_prefix}pattern .#{@class_prefix}#{name}" }
26
+ end
27
+
28
+ # @!visibility private
29
+ def escape_string(string)
30
+ CGI.escape_html(string)
31
+ end
32
+
33
+ # @!visibility private
34
+ def pre(type)
35
+ if @css == :inline
36
+ return "" unless rule = @target.theme[type]
37
+ "<#{@tag} style=\"#{rule.to_css_rule}\">"
38
+ else
39
+ "<#{@tag} class=\"#{@class_prefix}#{type}\">"
40
+ end
41
+ end
42
+
43
+ # @!visibility private
44
+ def post(type)
45
+ "</#{@tag}>"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ require 'mustermann/visualizer/renderer/generic'
2
+
3
+ module Mustermann
4
+ module Visualizer
5
+ # @!visibility private
6
+ module Renderer
7
+ # Generates a s-expression like string.
8
+ # @!visibility private
9
+ class Sexp < Generic
10
+ # @!visibility private
11
+ def render
12
+ @inspect = false
13
+ super.gsub(/ ?\)( \))*/) { |s| s.gsub(' ', '') }.strip
14
+ end
15
+
16
+
17
+ # @!visibility private
18
+ def pre(type)
19
+ "(#{type} " if type != :pattern
20
+ end
21
+
22
+ # @!visibility private
23
+ def escape_string(input)
24
+ inspect = input.inspect
25
+ input = inspect if inspect != "\"#{input}\""
26
+ input = inspect if input =~ /[\s\"\'\(\)]/
27
+ input + " "
28
+ end
29
+
30
+ # @!visibility private
31
+ def post(type)
32
+ ") " if type != :pattern
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ require 'hansi'
2
+
3
+ module Mustermann
4
+ module Visualizer
5
+ # Represents a (sub)tree and at the same time a node in the tree.
6
+ class Tree
7
+ # @!visibility private
8
+ attr_reader :line, :children, :prefix_color, :before, :after
9
+
10
+ # @!visibility private
11
+ def initialize(line, *children, prefix_color: :default, before: "", after: "")
12
+ @line = line
13
+ @children = children
14
+ @prefix_color = prefix_color
15
+ @before = before
16
+ @after = after
17
+ end
18
+
19
+ # used for positioning {#after}
20
+ # @!visibility private
21
+ def line_widths(offset = 0)
22
+ child_widths = children.flat_map { |c| c.line_widths(offset + 2) }
23
+ width = length(line + before) + offset
24
+ [width, *child_widths]
25
+ end
26
+
27
+ # Renders the tree.
28
+ # @return [String] rendered version of the tree
29
+ def to_s
30
+ render("", "", line_widths.max)
31
+ end
32
+
33
+ # Renders tree, including nesting.
34
+ # @!visibility private
35
+ def render(first_prefix, prefix, width)
36
+ output = before + Hansi.render(prefix_color, first_prefix) + line
37
+ output = ljust(output, width) + " " + after + "\n"
38
+ children[0..-2].each { |child| output += child.render(prefix + "├ ", prefix + "│ ", width) }
39
+ output += children.last.render(prefix + "└ ", prefix + " ", width) if children.last
40
+ output
41
+ end
42
+
43
+ # @!visibility private
44
+ def length(string)
45
+ deansi(string).length
46
+ end
47
+
48
+ # @!visibility private
49
+ def deansi(string)
50
+ string.gsub(/\e\[[^m]+m/, '')
51
+ end
52
+
53
+ # @!visibility private
54
+ def ljust(string, width)
55
+ missing = width - length(string)
56
+ append = missing > 0 ? " " * missing : ""
57
+ string + append
58
+ end
59
+
60
+ private :ljust, :deansi, :length
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,78 @@
1
+ require 'mustermann/visualizer/tree'
2
+ require 'mustermann/ast/translator'
3
+ require 'hansi'
4
+
5
+ module Mustermann
6
+ module Visualizer
7
+ # Turns an AST into a Tree
8
+ # @!visibility private
9
+ class TreeRenderer < AST::Translator
10
+ TEMPLATE = '"<base01>%s</base01><underline><green>%s</green></underline><base01>%s</base01>" '
11
+ THEME = Hansi::Theme[:solarized]
12
+ PREFIX_COLOR = THEME[:violet]
13
+ FakeNode = Struct.new(:type, :start, :stop, :length)
14
+ private_constant(:TEMPLATE, :THEME, :PREFIX_COLOR, :FakeNode)
15
+
16
+ # Takes a pattern (or pattern string and option) and turns it into a tree.
17
+ # Runs translation if pattern implements to_ast, otherwise returns single
18
+ # node tree.
19
+ #
20
+ # @!visibility private
21
+ def self.render(pattern, **options)
22
+ pattern &&= Mustermann.new(pattern, **options)
23
+ renderer = new(pattern.to_s)
24
+ if pattern.respond_to? :to_ast
25
+ renderer.translate(pattern.to_ast)
26
+ else
27
+ length = renderer.string.length
28
+ node = FakeNode.new("pattern (not AST based)", 0, length, length)
29
+ renderer.tree(node)
30
+ end
31
+ end
32
+
33
+ # @!visibility private
34
+ attr_reader :string
35
+
36
+ # @!visibility private
37
+ def initialize(string)
38
+ @string = string
39
+ end
40
+
41
+ # access a substring of the pattern, in inspect mode
42
+ # @!visibility private
43
+ def sub(*args)
44
+ string[*args].inspect[1..-2]
45
+ end
46
+
47
+ # creates a tree node
48
+ # @!visibility private
49
+ def tree(node, *children, **typed_children)
50
+ children += children_for(typed_children)
51
+ children = children.flatten.grep(Tree)
52
+ infos = sub(0, node.start), sub(node.start, node.length), sub(node.stop..-1)
53
+ description = Hansi.render(THEME[:green], node.type.to_s.tr("_", " "))
54
+ after = Hansi.render(TEMPLATE, *infos, theme: THEME, tags: true)
55
+ Tree.new(description, *children, after: after, prefix_color: PREFIX_COLOR)
56
+ end
57
+
58
+ # Take a hash with trees as values and turn the keys into trees, too.
59
+ # Read again if that didn't make sense.
60
+ # @!visibility private
61
+ def children_for(list)
62
+ list.map do |key, value|
63
+ value = Array(value).flatten
64
+ if value.any?
65
+ after = " " * string.inspect.length + " "
66
+ description = Hansi.render(THEME[:orange], key.to_s)
67
+ Tree.new(description, *value, after: after, prefix_color: PREFIX_COLOR)
68
+ end
69
+ end
70
+ end
71
+
72
+ translate(:node) { t.tree(node, payload: t(payload)) }
73
+ translate(:with_look_ahead) { t.tree(node, head: t(head), payload: t(payload)) }
74
+ translate(Array) { map { |e| t(e) }}
75
+ translate(Object) { }
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,19 @@
1
+ $:.unshift File.expand_path("../../mustermann/lib", __FILE__)
2
+ require "mustermann/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "mustermann-visualizer"
6
+ s.version = Mustermann::VERSION
7
+ s.author = "Konstantin Haase"
8
+ s.email = "konstantin.mailinglists@googlemail.com"
9
+ s.homepage = "https://github.com/rkh/mustermann"
10
+ s.summary = %q{Visualize Mustermann patterns}
11
+ s.description = %q{Provides syntax highlighting and other visualizations for Mustermman}
12
+ s.license = 'MIT'
13
+ s.required_ruby_version = '>= 2.1.0'
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.add_dependency 'mustermann', Mustermann::VERSION
18
+ s.add_dependency 'hansi', '~> 0.1.1'
19
+ end
@@ -0,0 +1,48 @@
1
+ require 'support'
2
+ require 'mustermann/visualizer'
3
+ require 'pp'
4
+ require 'stringio'
5
+
6
+ describe Mustermann::Visualizer::PatternExtension do
7
+ subject(:pattern) { Mustermann.new("/:name") }
8
+ before { Hansi.mode = 16 }
9
+ after { Hansi.mode = nil }
10
+
11
+ specify :to_ansi do
12
+ pattern.to_ansi(inspect: true, capture: :red, default: nil).should be == "\e[0m\"\e[0m/\e[0m\e[91m:\e[0m\e[91mname\e[0m\"\e[0m"
13
+ pattern.to_ansi(inspect: false, capture: :green, default: nil).should be == "\e[0m/\e[0m\e[32m:\e[0m\e[32mname\e[0m"
14
+ end
15
+
16
+ specify :to_html do
17
+ pattern.to_html(css: false, class_prefix: "", tag: :tt).should be == '<tt class="pattern"><tt class="root"><tt class="separator">/</tt><tt class="capture">:<tt class="name">name</tt></tt></tt></tt>'
18
+ end
19
+
20
+ specify :to_tree do
21
+ pattern.to_tree.should be == Mustermann::Visualizer.tree(pattern).to_s
22
+ end
23
+
24
+ specify :color_inspect do
25
+ pattern.color_inspect.should include(pattern.to_ansi(inspect: true))
26
+ pattern.color_inspect.should include("#<Mustermann::Sinatra:")
27
+ end
28
+
29
+ specify :to_s do
30
+ object = Class.new { def puts(arg) arg.to_s end }.new
31
+ object.puts(pattern).should be == pattern.to_ansi
32
+ end
33
+
34
+ context :pretty_print do
35
+ before(:all) { ColorPrinter = Class.new(::PP) }
36
+ let(:output) { StringIO.new }
37
+
38
+ specify 'with color printer' do
39
+ ColorPrinter.new(output, 79).pp(pattern)
40
+ output.string.should be == pattern.color_inspect
41
+ end
42
+
43
+ specify 'without color printer' do
44
+ ::PP.new(output, 79).pp(pattern)
45
+ output.string.should be == pattern.inspect
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,179 @@
1
+ require 'support'
2
+ require 'mustermann/visualizer'
3
+
4
+ describe Mustermann::Visualizer do
5
+ subject(:highlight) { Mustermann::Visualizer.highlight(pattern) }
6
+ before { Hansi.mode = 256 }
7
+ after { Hansi.mode = nil }
8
+
9
+ describe :highlight do
10
+ context :sinatra do
11
+ context "/a" do
12
+ let(:pattern) { Mustermann.new("/a") }
13
+ its(:to_ansi) { should be == "\e[0m\e[38;5;246m\e[38;5;246m\e[38;5;247m/\e[0m\e[38;5;246m\e[38;5;246m\e[38;5;246ma\e[0m" }
14
+ its(:to_html) { should be == '<span style="color: #839496;"><span style="color: #93a1a1;">/</span><span style="color: #839496;">a</span></span></span>' }
15
+ its(:to_sexp) { should be == '(root (separator /) (char a))' }
16
+ its(:to_pattern) { should be == pattern }
17
+ its(:to_s) { should be == "/a" }
18
+ its(:stylesheet) { should include(".mustermann_pattern .mustermann_illegal {\n color: #8b0000;") }
19
+
20
+ example do
21
+ highlight.to_html(css: false).should be ==
22
+ '<span class="mustermann_pattern"><span class="mustermann_root"><span class="mustermann_separator">/</span><span class="mustermann_char">a</span></span></span>'
23
+ end
24
+
25
+ example do
26
+ renderer = Mustermann::Visualizer::Renderer::Generic
27
+ result = highlight.render_with(renderer)
28
+ result.should be == pattern.to_s
29
+ end
30
+ end
31
+
32
+ context '/:name' do
33
+ let(:pattern) { Mustermann.new("/:name") }
34
+ its(:to_sexp) { should be == "(root (separator /) (capture : (name name)))" }
35
+ end
36
+
37
+ context '/{name}' do
38
+ let(:pattern) { Mustermann.new("/{name}") }
39
+ its(:to_sexp) { should be == "(root (separator /) (capture { (name name) }))" }
40
+ end
41
+
42
+ context '/{+name}' do
43
+ let(:pattern) { Mustermann.new("/{+name}") }
44
+ its(:to_sexp) { should be == "(root (separator /) (named_splat {+ (name name) }))" }
45
+ end
46
+
47
+ context ':user(@:host)?' do
48
+ let(:pattern) { Mustermann.new(':user(@:host)?') }
49
+ its(:to_sexp) { should be == '(root (capture : (name user)) (optional (group "(" (char @) (capture : (name host)) ")") ?))' }
50
+ end
51
+
52
+ context 'a b' do
53
+ let(:pattern) { Mustermann.new('a b') }
54
+ its(:to_sexp) { should be == '(root (char a) (char " ") (char b))' }
55
+ end
56
+
57
+ context '\:a' do
58
+ let(:pattern) { Mustermann.new('\:a') }
59
+ its(:to_sexp) { should be == '(root (escaped "\\\\" (escaped_char :)) (char a))' }
60
+ end
61
+ end
62
+
63
+ context :regexp do
64
+ context 'a' do
65
+ let(:pattern) { Mustermann.new('a', type: :regexp) }
66
+ its(:to_sexp) { should be == '(root (char a))' }
67
+ end
68
+
69
+ context '/(\d+)' do
70
+ let(:pattern) { Mustermann.new('/(\d+)', type: :regexp) }
71
+ its(:to_sexp) { should be == '(root (separator /) (capture "(" (special "\\\\d") (special +))))' }
72
+ end
73
+
74
+ context '\A' do
75
+ let(:pattern) { Mustermann.new('\A', type: :regexp) }
76
+ its(:to_sexp) { should be == '(root (illegal "\\\\A"))' }
77
+ end
78
+
79
+ context '(?<name>.)\g<name>' do
80
+ let(:pattern) { Mustermann.new('(?<name>.)\g<name>', type: :regexp) }
81
+ its(:to_sexp) { should be == '(root (capture "(?<" (name) >(special .))) (special "\\\\g<name>"))' }
82
+ end
83
+
84
+ context '\p{Ll}' do
85
+ let(:pattern) { Mustermann.new('\p{Ll}', type: :regexp) }
86
+ its(:to_sexp) { should be == '(root (special "\\\\p{Ll}"))' }
87
+ end
88
+
89
+ context '\/' do
90
+ let(:pattern) { Mustermann.new('\/', type: :regexp) }
91
+ its(:to_sexp) { should be == '(root (separator /))' }
92
+ end
93
+
94
+ context '\[' do
95
+ let(:pattern) { Mustermann.new('\[', type: :regexp) }
96
+ its(:to_sexp) { should be == '(root (escaped "\\\\" (escaped_char [)))' }
97
+ end
98
+
99
+ context '^' do
100
+ let(:pattern) { Mustermann.new('^', type: :regexp) }
101
+ its(:to_sexp) { should be == '(root (illegal ^))' }
102
+ end
103
+
104
+ context '(?-mix:.)' do
105
+ let(:pattern) { Mustermann.new('(?-mix:.)', type: :regexp) }
106
+ its(:to_sexp) { should be == '(root (special "(") (special .) (special ")"))' }
107
+ end
108
+
109
+ context '[a\d]' do
110
+ let(:pattern) { Mustermann.new('[a\d]', type: :regexp) }
111
+ its(:to_sexp) { should be == '(root (special [) (char a) (special "\\\\d") (special ]))' }
112
+ end
113
+
114
+ context '[^a-z]' do
115
+ let(:pattern) { Mustermann.new('[^a-z]', type: :regexp) }
116
+ its(:to_sexp) { should be == '(root (special [) (special ^) (char a) (special -) (char z) (special ]))' }
117
+ end
118
+
119
+ context '[[:digit:]]' do
120
+ let(:pattern) { Mustermann.new('[[:digit:]]', type: :regexp) }
121
+ its(:to_sexp) { should be == '(root (special [[:digit:]]))' }
122
+ end
123
+
124
+ context 'a{1,}' do
125
+ let(:pattern) { Mustermann.new('a{1,}', type: :regexp) }
126
+ its(:to_sexp) { should be == "(root (char a) (special {1,}))" }
127
+ end
128
+ end
129
+
130
+ context :template do
131
+ context '/{name}' do
132
+ let(:pattern) { Mustermann.new("/{+foo,bar*}", type: :template) }
133
+ its(:to_sexp) { should be == "(root (separator /) (expression {+ (variable (name foo)) , (variable (name bar) *) }))" }
134
+ end
135
+ end
136
+
137
+ context "custom AST based pattern" do
138
+ let(:my_type) { Class.new(Mustermann::AST::Pattern) { on('x') { |*| node(:char, "o") } }}
139
+ let(:pattern) { Mustermann.new("fxx", type: my_type) }
140
+ its(:to_sexp) { should be == "(root (char f) (escaped x) (escaped x))" }
141
+ end
142
+
143
+ context "without known highlighter" do
144
+ let(:pattern) { Mustermann::Pattern.new("foo") }
145
+ its(:to_sexp) { should be == "(root (unknown foo))" }
146
+ end
147
+ end
148
+
149
+ describe :tree do
150
+ subject(:tree) { Mustermann::Visualizer.tree(pattern) }
151
+
152
+ context :sinatra do
153
+ context "/:a(@:b)" do
154
+ let(:pattern) { Mustermann.new("/:a(@:b)") }
155
+ let(:tree_data) do
156
+ <<-TREE.gsub(/^\s+/, '')
157
+ \e[38;5;61m\e[0m\e[38;5;100mroot\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\e[4m\e[38;5;100m/:a(@:b)\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\" \e[0m
158
+ \e[38;5;61m└ \e[0m\e[38;5;166mpayload\e[0m
159
+ \e[38;5;61m ├ \e[0m\e[38;5;100mseparator\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\e[4m\e[38;5;100m/\e[0m\e[38;5;66m\e[38;5;242m:a(@:b)\e[0m\e[38;5;66m\" \e[0m
160
+ \e[38;5;61m ├ \e[0m\e[38;5;100mcapture\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/\e[0m\e[38;5;66m\e[4m\e[38;5;100m:a\e[0m\e[38;5;66m\e[38;5;242m(@:b)\e[0m\e[38;5;66m\" \e[0m
161
+ \e[38;5;61m └ \e[0m\e[38;5;100mgroup\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/:a\e[0m\e[38;5;66m\e[4m\e[38;5;100m(@:b)\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\" \e[0m
162
+ \e[38;5;61m └ \e[0m\e[38;5;166mpayload\e[0m
163
+ \e[38;5;61m ├ \e[0m\e[38;5;100mchar\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/:a(\e[0m\e[38;5;66m\e[4m\e[38;5;100m@\e[0m\e[38;5;66m\e[38;5;242m:b)\e[0m\e[38;5;66m\" \e[0m
164
+ \e[38;5;61m └ \e[0m\e[38;5;100mcapture\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/:a(@\e[0m\e[38;5;66m\e[4m\e[38;5;100m:b\e[0m\e[38;5;66m\e[38;5;242m)\e[0m\e[38;5;66m\" \e[0m
165
+ TREE
166
+ end
167
+ its(:to_s) { should be == tree_data }
168
+ end
169
+ end
170
+
171
+ context :shell do
172
+ context "/**/*" do
173
+ let(:pattern) { Mustermann.new("/**/*", type: :shell) }
174
+ let(:tree_data) { "\e[38;5;61m\e[0m\e[38;5;100mpattern (not AST based)\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\e[4m\e[38;5;100m/**/*\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\" \e[0m\n" }
175
+ its(:to_s) { should be == tree_data }
176
+ end
177
+ end
178
+ end
179
+ end