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,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