mustermann 1.0.1 → 1.0.2.rc1

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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +18 -0
  3. data/.rspec +5 -0
  4. data/.travis.yml +25 -0
  5. data/.yardopts +3 -0
  6. data/Gemfile +7 -0
  7. data/README.md +230 -799
  8. data/Rakefile +27 -0
  9. data/mustermann-contrib/LICENSE +23 -0
  10. data/mustermann-contrib/README.md +1155 -0
  11. data/mustermann-contrib/examples/highlighting.rb +35 -0
  12. data/mustermann-contrib/highlighting.png +0 -0
  13. data/mustermann-contrib/irb.png +0 -0
  14. data/mustermann-contrib/lib/mustermann/cake.rb +19 -0
  15. data/mustermann-contrib/lib/mustermann/express.rb +38 -0
  16. data/mustermann-contrib/lib/mustermann/file_utils.rb +218 -0
  17. data/mustermann-contrib/lib/mustermann/file_utils/glob_pattern.rb +40 -0
  18. data/mustermann-contrib/lib/mustermann/fileutils.rb +1 -0
  19. data/mustermann-contrib/lib/mustermann/flask.rb +199 -0
  20. data/mustermann-contrib/lib/mustermann/pyramid.rb +29 -0
  21. data/mustermann-contrib/lib/mustermann/rails.rb +47 -0
  22. data/mustermann-contrib/lib/mustermann/shell.rb +57 -0
  23. data/mustermann-contrib/lib/mustermann/simple.rb +51 -0
  24. data/mustermann-contrib/lib/mustermann/string_scanner.rb +314 -0
  25. data/mustermann-contrib/lib/mustermann/strscan.rb +1 -0
  26. data/mustermann-contrib/lib/mustermann/template.rb +63 -0
  27. data/mustermann-contrib/lib/mustermann/uri_template.rb +1 -0
  28. data/mustermann-contrib/lib/mustermann/versions.rb +47 -0
  29. data/mustermann-contrib/lib/mustermann/visualizer.rb +39 -0
  30. data/mustermann-contrib/lib/mustermann/visualizer/highlight.rb +138 -0
  31. data/mustermann-contrib/lib/mustermann/visualizer/highlighter.rb +38 -0
  32. data/mustermann-contrib/lib/mustermann/visualizer/highlighter/ad_hoc.rb +95 -0
  33. data/mustermann-contrib/lib/mustermann/visualizer/highlighter/ast.rb +103 -0
  34. data/mustermann-contrib/lib/mustermann/visualizer/highlighter/composite.rb +46 -0
  35. data/mustermann-contrib/lib/mustermann/visualizer/highlighter/dummy.rb +19 -0
  36. data/mustermann-contrib/lib/mustermann/visualizer/highlighter/regular.rb +105 -0
  37. data/mustermann-contrib/lib/mustermann/visualizer/pattern_extension.rb +69 -0
  38. data/mustermann-contrib/lib/mustermann/visualizer/renderer/ansi.rb +24 -0
  39. data/mustermann-contrib/lib/mustermann/visualizer/renderer/generic.rb +47 -0
  40. data/mustermann-contrib/lib/mustermann/visualizer/renderer/hansi_template.rb +35 -0
  41. data/mustermann-contrib/lib/mustermann/visualizer/renderer/html.rb +51 -0
  42. data/mustermann-contrib/lib/mustermann/visualizer/renderer/sexp.rb +38 -0
  43. data/mustermann-contrib/lib/mustermann/visualizer/tree.rb +64 -0
  44. data/mustermann-contrib/lib/mustermann/visualizer/tree_renderer.rb +79 -0
  45. data/mustermann-contrib/mustermann-contrib.gemspec +19 -0
  46. data/mustermann-contrib/spec/cake_spec.rb +91 -0
  47. data/mustermann-contrib/spec/express_spec.rb +210 -0
  48. data/mustermann-contrib/spec/file_utils_spec.rb +120 -0
  49. data/mustermann-contrib/spec/flask_spec.rb +362 -0
  50. data/mustermann-contrib/spec/flask_subclass_spec.rb +369 -0
  51. data/mustermann-contrib/spec/pattern_extension_spec.rb +50 -0
  52. data/mustermann-contrib/spec/pyramid_spec.rb +102 -0
  53. data/mustermann-contrib/spec/rails_spec.rb +648 -0
  54. data/mustermann-contrib/spec/shell_spec.rb +148 -0
  55. data/mustermann-contrib/spec/simple_spec.rb +269 -0
  56. data/mustermann-contrib/spec/string_scanner_spec.rb +272 -0
  57. data/mustermann-contrib/spec/template_spec.rb +842 -0
  58. data/mustermann-contrib/spec/visualizer_spec.rb +199 -0
  59. data/mustermann-contrib/theme.png +0 -0
  60. data/mustermann-contrib/tree.png +0 -0
  61. data/mustermann/LICENSE +23 -0
  62. data/mustermann/README.md +853 -0
  63. data/{bench → mustermann/bench}/capturing.rb +0 -0
  64. data/{bench → mustermann/bench}/regexp.rb +0 -0
  65. data/{bench → mustermann/bench}/simple_vs_sinatra.rb +0 -0
  66. data/{bench → mustermann/bench}/template_vs_addressable.rb +0 -0
  67. data/{lib → mustermann/lib}/mustermann.rb +0 -0
  68. data/{lib → mustermann/lib}/mustermann/ast/boundaries.rb +0 -0
  69. data/{lib → mustermann/lib}/mustermann/ast/compiler.rb +0 -0
  70. data/{lib → mustermann/lib}/mustermann/ast/expander.rb +0 -0
  71. data/{lib → mustermann/lib}/mustermann/ast/node.rb +0 -0
  72. data/{lib → mustermann/lib}/mustermann/ast/param_scanner.rb +0 -0
  73. data/{lib → mustermann/lib}/mustermann/ast/parser.rb +0 -0
  74. data/{lib → mustermann/lib}/mustermann/ast/pattern.rb +0 -0
  75. data/{lib → mustermann/lib}/mustermann/ast/template_generator.rb +0 -0
  76. data/{lib → mustermann/lib}/mustermann/ast/transformer.rb +0 -0
  77. data/{lib → mustermann/lib}/mustermann/ast/translator.rb +0 -0
  78. data/{lib → mustermann/lib}/mustermann/ast/validation.rb +0 -0
  79. data/{lib → mustermann/lib}/mustermann/caster.rb +0 -0
  80. data/{lib → mustermann/lib}/mustermann/composite.rb +0 -0
  81. data/{lib → mustermann/lib}/mustermann/concat.rb +13 -2
  82. data/{lib → mustermann/lib}/mustermann/equality_map.rb +0 -0
  83. data/{lib → mustermann/lib}/mustermann/error.rb +0 -0
  84. data/{lib → mustermann/lib}/mustermann/expander.rb +0 -0
  85. data/{lib → mustermann/lib}/mustermann/extension.rb +0 -0
  86. data/{lib → mustermann/lib}/mustermann/identity.rb +0 -0
  87. data/{lib → mustermann/lib}/mustermann/mapper.rb +0 -0
  88. data/{lib → mustermann/lib}/mustermann/pattern.rb +1 -1
  89. data/{lib → mustermann/lib}/mustermann/pattern_cache.rb +0 -0
  90. data/{lib → mustermann/lib}/mustermann/regexp.rb +0 -0
  91. data/{lib → mustermann/lib}/mustermann/regexp_based.rb +0 -0
  92. data/{lib → mustermann/lib}/mustermann/regular.rb +0 -0
  93. data/{lib → mustermann/lib}/mustermann/simple_match.rb +0 -0
  94. data/{lib → mustermann/lib}/mustermann/sinatra.rb +1 -1
  95. data/{lib → mustermann/lib}/mustermann/sinatra/parser.rb +0 -0
  96. data/{lib → mustermann/lib}/mustermann/sinatra/safe_renderer.rb +0 -0
  97. data/{lib → mustermann/lib}/mustermann/sinatra/try_convert.rb +0 -0
  98. data/{lib → mustermann/lib}/mustermann/to_pattern.rb +0 -0
  99. data/{lib → mustermann/lib}/mustermann/version.rb +1 -1
  100. data/{mustermann.gemspec → mustermann/mustermann.gemspec} +0 -0
  101. data/{spec → mustermann/spec}/ast_spec.rb +0 -0
  102. data/{spec → mustermann/spec}/composite_spec.rb +0 -0
  103. data/{spec → mustermann/spec}/concat_spec.rb +12 -0
  104. data/{spec → mustermann/spec}/equality_map_spec.rb +0 -0
  105. data/{spec → mustermann/spec}/expander_spec.rb +0 -0
  106. data/{spec → mustermann/spec}/extension_spec.rb +0 -0
  107. data/{spec → mustermann/spec}/identity_spec.rb +0 -0
  108. data/{spec → mustermann/spec}/mapper_spec.rb +0 -0
  109. data/{spec → mustermann/spec}/mustermann_spec.rb +0 -0
  110. data/{spec → mustermann/spec}/pattern_spec.rb +0 -0
  111. data/{spec → mustermann/spec}/regexp_based_spec.rb +0 -0
  112. data/{spec → mustermann/spec}/regular_spec.rb +0 -0
  113. data/{spec → mustermann/spec}/simple_match_spec.rb +0 -0
  114. data/{spec → mustermann/spec}/sinatra_spec.rb +0 -0
  115. data/{spec → mustermann/spec}/to_pattern_spec.rb +0 -0
  116. data/support/lib/support.rb +7 -0
  117. data/support/lib/support/coverage.rb +23 -0
  118. data/support/lib/support/env.rb +19 -0
  119. data/support/lib/support/expand_matcher.rb +28 -0
  120. data/support/lib/support/generate_template_matcher.rb +27 -0
  121. data/support/lib/support/match_matcher.rb +39 -0
  122. data/support/lib/support/pattern.rb +42 -0
  123. data/support/lib/support/projects.rb +20 -0
  124. data/support/lib/support/scan_matcher.rb +63 -0
  125. data/support/support.gemspec +27 -0
  126. metadata +128 -58
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ module Mustermann
3
+ module Visualizer
4
+ # @!visibility private
5
+ module Highlighter
6
+ # @!visibility private
7
+ module Composite
8
+ extend self
9
+
10
+ # @!visibility private
11
+ def highlight?(pattern)
12
+ pattern.is_a? Mustermann::Composite
13
+ end
14
+
15
+ # @!visibility private
16
+ def highlight(pattern, renderer)
17
+ operator = " #{pattern.operator} "
18
+ patterns = pattern.patterns.map { |p| highlight_nested(p, renderer) }.join(quote(renderer, operator))
19
+ renderer.pre(:composite) + patterns + renderer.post(:composite)
20
+ end
21
+
22
+ # @!visibility private
23
+ def highlight_nested(pattern, renderer)
24
+ highlighter = Highlighter.highlighter_for(pattern)
25
+ if highlighter.respond_to? :nested_highlight
26
+ highlighter.nested_highlight(pattern, renderer)
27
+ else
28
+ type = quote(renderer, pattern.class.name[/[^:]+$/].downcase + ":", :type)
29
+ quote = quote(renderer, ?")
30
+ type + quote + highlighter.highlight(pattern, renderer) + quote
31
+ end
32
+ end
33
+
34
+ # @!visibility private
35
+ def nested_highlight(pattern, renderer)
36
+ quote(renderer, ?() + highlight(pattern, renderer) + quote(renderer, ?))
37
+ end
38
+
39
+ # @!visibility private
40
+ def quote(renderer, string, type = :quote)
41
+ renderer.pre(type) + renderer.escape(string, string) + renderer.post(type)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ module Mustermann
3
+ module Visualizer
4
+ # @!visibility private
5
+ module Highlighter
6
+ # Provides highlighting for patterns that don't have a highlighter.
7
+ # @!visibility private
8
+ module Dummy
9
+ # @!visibility private
10
+ def self.highlight(pattern, renderer)
11
+ output = String.new
12
+ output << renderer.pre(:root) << renderer.pre(:unknown)
13
+ output << renderer.escape(pattern.to_s)
14
+ output << renderer.post(:unknown) << renderer.post(:root)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+ require 'strscan'
3
+
4
+ module Mustermann
5
+ module Visualizer
6
+ # @!visibility private
7
+ module Highlighter
8
+ # Provides highlighting for {Mustermann::Regular}
9
+ # @!visibility private
10
+ class Regular
11
+ # @!visibility private
12
+ SPECIAL_ESCAPE = ['w', 'W', 'd', 'D', 'h', 'H', 's', 'S', 'G', 'b', 'B']
13
+ private_constant(:SPECIAL_ESCAPE)
14
+
15
+ # @!visibility private
16
+ def self.highlight?(pattern)
17
+ pattern.class.name == "Mustermann::Regular"
18
+ end
19
+
20
+ # @!visibility private
21
+ def self.highlight(pattern, renderer)
22
+ new(renderer).highlight(pattern)
23
+ end
24
+
25
+ # @!visibility private
26
+ attr_reader :renderer, :output, :scanner
27
+
28
+ # @!visibility private
29
+ def initialize(renderer)
30
+ @renderer = renderer
31
+ @output = String.new
32
+ end
33
+
34
+ # @!visibility private
35
+ def highlight(pattern)
36
+ output << renderer.pre(:root)
37
+ @scanner = ::StringScanner.new(pattern.to_s)
38
+ scan
39
+ output << renderer.post(:root)
40
+ end
41
+
42
+ # @!visibility private
43
+ def scan(stop = nil)
44
+ until scanner.eos?
45
+ case char = scanner.getch
46
+ when stop then return char
47
+ when ?/ then element(:separator, char)
48
+ when Regexp.escape(char) then element(:char, char)
49
+ when ?\\ then escaped(scanner.getch)
50
+ when ?( then potential_capture
51
+ when ?[ then char_class
52
+ when ?^, ?$ then element(:illegal, char)
53
+ when ?{ then element(:special, "\{#{scanner.scan(/[^\}]*\}/)}")
54
+ else element(:special, char)
55
+ end
56
+ end
57
+ end
58
+
59
+ # @!visibility private
60
+ def char_class
61
+ if result = scanner.scan(/\[:\w+:\]\]/)
62
+ element(:special, "[#{result}")
63
+ else
64
+ element(:special, ?[)
65
+ element(:special, ?^) if scanner.scan(/\^/)
66
+ end
67
+ end
68
+
69
+ # @!visibility private
70
+ def potential_capture
71
+ if scanner.scan(/\?<(\w+)>/)
72
+ element(:capture, "(?<") do
73
+ element(:name, scanner[1])
74
+ output << ">" << scan(?))
75
+ end
76
+ elsif scanner.scan(/\?(?:(?:-\w+)?:|>|<=|<!|!|=)/)
77
+ element(:special, "(#{scanner[0]}")
78
+ else
79
+ element(:capture, "(") { output << scan(?)) }
80
+ end
81
+ end
82
+
83
+ # @!visibility private
84
+ def escaped(char)
85
+ case char
86
+ when *SPECIAL_ESCAPE then element(:special, "\\#{char}")
87
+ when 'A', 'Z', 'z' then element(:illegal, "\\#{char}")
88
+ when 'g' then element(:special, "\\#{char}#{scanner.scan(/<\w*>/)}")
89
+ when 'p', 'u' then element(:special, "\\#{char}#{scanner.scan(/\{[^\}]*\}/)}")
90
+ when ?/ then element(:separator, char)
91
+ else element(:escaped, ?\\) { element(:escaped_char, char) }
92
+ end
93
+ end
94
+
95
+ # @!visibility private
96
+ def element(type, content = nil)
97
+ output << renderer.pre(type)
98
+ output << renderer.escape(content) if content
99
+ yield if block_given?
100
+ output << renderer.post(type)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ module Mustermann
3
+ module Visualizer
4
+ # Mixin that will be added to {Mustermann::Pattern}.
5
+ module PatternExtension
6
+ prepend_features Composite
7
+ prepend_features Pattern
8
+
9
+ # @example
10
+ # puts Mustermann.new("/:page").to_ansi
11
+ #
12
+ # @return [String] ANSI colorized version of the pattern.
13
+ def to_ansi(inspect: nil, **theme)
14
+ Visualizer.highlight(self, **theme).to_ansi(inspect: inspect)
15
+ end
16
+
17
+ # @example
18
+ # puts Mustermann.new("/:page").to_html
19
+ #
20
+ # @return [String] HTML version of the pattern.
21
+ def to_html(inspect: nil, tag: :span, class_prefix: "mustermann_", css: :inline, **theme)
22
+ Visualizer.highlight(self, **theme).to_html(inspect: inspect, tag: tag, class_prefix: class_prefix, css: css)
23
+ end
24
+
25
+ # @example
26
+ # puts Mustermann.new("/:page").to_tree
27
+ #
28
+ # @return [String] tree version of the pattern.
29
+ def to_tree
30
+ Visualizer.tree(self).to_s
31
+ end
32
+
33
+ # If invoked directly by puts: ANSI colorized version of the pattern.
34
+ # If invoked by anything else: String version of the pattern.
35
+ #
36
+ # @example
37
+ # require 'mustermann/visualizer'
38
+ # pattern = Mustermann.new('/:page')
39
+ # puts pattern # will have color
40
+ # puts pattern.to_s # will not have color
41
+ #
42
+ # @return [String] non-colorized or colorized version of the pattern
43
+ def to_s
44
+ caller_locations.first.label == 'puts' ? to_ansi : super
45
+ end
46
+
47
+ # If invoked directly by IRB, same as {#color_inspect}, otherwise same as {Mustermann::Pattern#inspect}.
48
+ def inspect
49
+ caller_locations.first.base_label == '<module:IRB>' ? color_inspect : super
50
+ end
51
+
52
+ # @return [String] ANSI colorized version of {Mustermann::Pattern#inspect}
53
+ def color_inspect(base_color = nil, **theme)
54
+ base_color ||= Highlight::DEFAULT_THEME[:base01]
55
+ template = is_a?(Composite) ? "*#<%p:(*%s*)>*" : "*#<%p:*%s*>*"
56
+ Hansi.render(template, self.class, to_ansi(inspect: true, **theme), "*" => base_color)
57
+ end
58
+
59
+ # If invoked directly by IRB, same as {#color_inspect}, otherwise same as Object#pretty_print.
60
+ def pretty_print(q)
61
+ if q.class.name.to_s[/[^:]+$/] == "ColorPrinter"
62
+ q.text(color_inspect, inspect.length)
63
+ else
64
+ super
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ module Mustermann
3
+ module Visualizer
4
+ # @!visibility private
5
+ module Renderer
6
+ # Generates ANSI colored strings.
7
+ # @!visibility private
8
+ class ANSI
9
+ # @!visibility private
10
+ def initialize(target, mode: Hansi.mode, **options)
11
+ @target = target
12
+ @mode = mode
13
+ @options = options
14
+ end
15
+
16
+ # @!visibility private
17
+ def render
18
+ template = @target.to_hansi_template(**@options)
19
+ Hansi.render(template, tags: true, theme: @target.theme, mode: @mode)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ module Mustermann
3
+ module Visualizer
4
+ # @!visibility private
5
+ module Renderer
6
+ # Logic shared by most renderers.
7
+ class Generic
8
+ # @!visibility private
9
+ def initialize(target, inspect: nil, add_qoutes: true)
10
+ @target = target
11
+ @inspect = inspect
12
+ @add_qoutes = !target.pattern.is_a?(Mustermann::Composite)
13
+ end
14
+
15
+ # @!visibility private
16
+ def render
17
+ quote = "#{pre(:quote)}#{escape_string(?")}#{post(:quote)}" if @inspect and @add_qoutes
18
+ pre(:pattern).to_s + preamble.to_s + quote.to_s + @target.render(self) + quote.to_s + post(:pattern).to_s
19
+ end
20
+
21
+ # @!visibility private
22
+ def preamble
23
+ end
24
+
25
+ # @!visibility private
26
+ def escape(value, inspect_value = value.to_s.inspect[1..-2])
27
+ escape_string(@inspect ? inspect_value : value.to_s)
28
+ end
29
+
30
+ # @!visibility private
31
+ def escape_string(string)
32
+ string
33
+ end
34
+
35
+ # @!visibility private
36
+ def pre(type)
37
+ ""
38
+ end
39
+
40
+ # @!visibility private
41
+ def post(type)
42
+ ""
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ require 'mustermann/visualizer/renderer/generic'
3
+
4
+ module Mustermann
5
+ module Visualizer
6
+ # @!visibility private
7
+ module Renderer
8
+ # Generates Hansi template string.
9
+ # @see Mustermann::Visualizer::Renderer::ANSI
10
+ # @!visibility private
11
+ class HansiTemplate < Generic
12
+ # @!visibility private
13
+ def initialize(*)
14
+ @hansi = Hansi::StringRenderer.new(tags: true)
15
+ super
16
+ end
17
+
18
+ # @!visibility private
19
+ def escape_string(string)
20
+ @hansi.escape(string)
21
+ end
22
+
23
+ # @!visibility private
24
+ def pre(type)
25
+ "<#{type}>"
26
+ end
27
+
28
+ # @!visibility private
29
+ def post(type)
30
+ "</#{type}>"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ require 'mustermann/visualizer/renderer/generic'
3
+ require 'cgi'
4
+
5
+ module Mustermann
6
+ module Visualizer
7
+ # @!visibility private
8
+ module Renderer
9
+ # Generates HTML output.
10
+ # @!visibility private
11
+ class HTML < Generic
12
+ # @!visibility private
13
+ def initialize(target, tag: :span, class_prefix: "mustermann_", css: :inline, **options)
14
+ raise ArgumentError, 'css option %p not supported, should be true, false or inline' if css != true and css != false and css != :inline
15
+ super(target, **options)
16
+ @css, @tag, @class_prefix = css, tag, class_prefix
17
+ end
18
+
19
+ # @!visibility private
20
+ def preamble
21
+ "<style type=\"text/css\">\n%s</style>" % stylesheet if @css == true
22
+ end
23
+
24
+ # @!visibility private
25
+ def stylesheet
26
+ @target.theme.to_css { |name| ".#{@class_prefix}pattern .#{@class_prefix}#{name}" }
27
+ end
28
+
29
+ # @!visibility private
30
+ def escape_string(string)
31
+ CGI.escape_html(string)
32
+ end
33
+
34
+ # @!visibility private
35
+ def pre(type)
36
+ if @css == :inline
37
+ return "" unless rule = @target.theme[type]
38
+ "<#{@tag} style=\"#{rule.to_css_rule}\">"
39
+ else
40
+ "<#{@tag} class=\"#{@class_prefix}#{type}\">"
41
+ end
42
+ end
43
+
44
+ # @!visibility private
45
+ def post(type)
46
+ "</#{@tag}>"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ require 'mustermann/visualizer/renderer/generic'
3
+
4
+ module Mustermann
5
+ module Visualizer
6
+ # @!visibility private
7
+ module Renderer
8
+ # Generates a s-expression like string.
9
+ # @!visibility private
10
+ class Sexp < Generic
11
+ # @!visibility private
12
+ def render
13
+ @inspect = false
14
+ super.gsub(/ ?\)( \))*/) { |s| s.gsub(' ', '') }.strip
15
+ end
16
+
17
+
18
+ # @!visibility private
19
+ def pre(type)
20
+ "(#{type} " if type != :pattern
21
+ end
22
+
23
+ # @!visibility private
24
+ def escape_string(input)
25
+ inspect = input.inspect
26
+ input = inspect if inspect != "\"#{input}\""
27
+ input = inspect if input =~ /[\s\"\'\(\)]/
28
+ input + " "
29
+ end
30
+
31
+ # @!visibility private
32
+ def post(type)
33
+ ") " if type != :pattern
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ require 'hansi'
3
+
4
+ module Mustermann
5
+ module Visualizer
6
+ # Represents a (sub)tree and at the same time a node in the tree.
7
+ class Tree
8
+ # @!visibility private
9
+ attr_reader :line, :children, :prefix_color, :before, :after
10
+
11
+ # @!visibility private
12
+ def initialize(line, *children, prefix_color: :default, before: "", after: "")
13
+ @line = line
14
+ @children = children
15
+ @prefix_color = prefix_color
16
+ @before = before
17
+ @after = after
18
+ end
19
+
20
+ # used for positioning {#after}
21
+ # @!visibility private
22
+ def line_widths(offset = 0)
23
+ child_widths = children.flat_map { |c| c.line_widths(offset + 2) }
24
+ width = length(line + before) + offset
25
+ [width, *child_widths]
26
+ end
27
+
28
+ # Renders the tree.
29
+ # @return [String] rendered version of the tree
30
+ def to_s
31
+ render("", "", line_widths.max)
32
+ end
33
+
34
+ # Renders tree, including nesting.
35
+ # @!visibility private
36
+ def render(first_prefix, prefix, width)
37
+ output = before + Hansi.render(prefix_color, first_prefix) + line
38
+ output = ljust(output, width) + " " + after + "\n"
39
+ children[0..-2].each { |child| output += child.render(prefix + "├ ", prefix + "│ ", width) }
40
+ output += children.last.render(prefix + "└ ", prefix + " ", width) if children.last
41
+ output
42
+ end
43
+
44
+ # @!visibility private
45
+ def length(string)
46
+ deansi(string).length
47
+ end
48
+
49
+ # @!visibility private
50
+ def deansi(string)
51
+ string.gsub(/\e\[[^m]+m/, '')
52
+ end
53
+
54
+ # @!visibility private
55
+ def ljust(string, width)
56
+ missing = width - length(string)
57
+ append = missing > 0 ? " " * missing : ""
58
+ string + append
59
+ end
60
+
61
+ private :ljust, :deansi, :length
62
+ end
63
+ end
64
+ end