bade 0.2.5 → 0.3.2

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.
@@ -83,9 +83,10 @@ class String
83
83
  count = 0
84
84
 
85
85
  each_char do |char|
86
- if char == SPACE_CHAR
86
+ case char
87
+ when SPACE_CHAR
87
88
  count += 1
88
- elsif char == TAB_CHAR
89
+ when TAB_CHAR
89
90
  count += tabsize
90
91
  else
91
92
  break
@@ -94,13 +95,4 @@ class String
94
95
 
95
96
  count
96
97
  end
97
-
98
- # source: http://apidock.com/rails/String/strip_heredoc
99
- # @return [String]
100
- #
101
- def strip_heredoc
102
- min_val = scan(/^[ \t]*(?=\S)/).min
103
- indent = (min_val && min_val.size) || 0
104
- gsub(/^[ \t]{#{indent}}/, '')
105
- end
106
98
  end
@@ -4,8 +4,6 @@ require_relative '../ruby2_keywords'
4
4
 
5
5
  module Bade
6
6
  module Runtime
7
- class RuntimeError < ::StandardError; end
8
-
9
7
  class Block
10
8
  class MissingBlockDefinitionError < RuntimeError
11
9
  # @return [String]
@@ -16,8 +14,8 @@ module Bade
16
14
  #
17
15
  attr_accessor :context
18
16
 
19
- def initialize(name, context, msg = nil)
20
- super()
17
+ def initialize(name, context, msg, template_backtrace)
18
+ super(msg, template_backtrace)
21
19
 
22
20
  self.name = name
23
21
  self.context = context
@@ -38,34 +36,51 @@ module Bade
38
36
  #
39
37
  attr_reader :name
40
38
 
39
+ # @return [RenderBinding::Location, nil]
40
+ #
41
+ attr_reader :location
42
+
41
43
  # @return [RenderBinding]
42
44
  #
43
45
  attr_reader :render_binding
44
46
 
45
47
  # @param [String] name name of the block
48
+ # @param [RenderBinding::Location, nil] location
46
49
  # @param [RenderBinding] render_binding reference to current binding instance
47
50
  # @param [Proc] block reference to lambda
48
51
  #
49
- def initialize(name, render_binding, &block)
52
+ def initialize(name, location, render_binding, &block)
50
53
  @name = name
54
+ @location = location
51
55
  @render_binding = render_binding
52
56
  @block = block
53
57
  end
54
58
 
55
59
  # --- Calling methods
56
60
 
61
+ # Calls the block and adds rendered content into current buffer stack.
62
+ #
63
+ # @return [Void]
57
64
  ruby2_keywords def call(*args)
58
65
  call!(*args) unless @block.nil?
59
66
  end
60
67
 
68
+ # Calls the block and adds rendered content into current buffer stack.
69
+ #
70
+ # @return [Void]
61
71
  ruby2_keywords def call!(*args)
62
- raise MissingBlockDefinitionError.new(name, :call) if @block.nil?
72
+ raise MissingBlockDefinitionError.new(name, :call, nil, render_binding.__location_stack) if @block.nil?
63
73
 
64
- render_binding.__buff.concat(@block.call(*args))
74
+ __call(*args)
65
75
  end
66
76
 
67
77
  # --- Rendering methods
68
78
 
79
+ # Calls the block and returns rendered content in string.
80
+ #
81
+ # Returns empty string when there is no block.
82
+ #
83
+ # @return [String]
69
84
  def render(*args)
70
85
  if @block.nil?
71
86
  ''
@@ -74,10 +89,33 @@ module Bade
74
89
  end
75
90
  end
76
91
 
92
+ # Calls the block and returns rendered content in string.
93
+ #
94
+ # Throws error when there is no block.
95
+ #
96
+ # @return [String]
77
97
  def render!(*args)
78
- raise MissingBlockDefinitionError.new(name, :render) if @block.nil?
98
+ raise MissingBlockDefinitionError.new(name, :render, nil, render_binding.__location_stack) if @block.nil?
99
+
100
+ loc = location.dup
101
+ render_binding.__buffs_push(loc)
102
+
103
+ @block.call(*args)
104
+
105
+ render_binding.__buffs_pop&.join || ''
106
+ end
107
+
108
+ # Calls the block and adds rendered content into current buffer stack.
109
+ #
110
+ # @return [Void]
111
+ ruby2_keywords def __call(*args)
112
+ loc = location.dup
113
+ render_binding.__buffs_push(loc)
114
+
115
+ @block.call(*args)
79
116
 
80
- @block.call(*args).join
117
+ res = render_binding.__buffs_pop
118
+ render_binding.__buff&.concat(res) if !res.nil? && !res.empty?
81
119
  end
82
120
  end
83
121
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils/where'
4
+
5
+ module Bade
6
+ module Runtime
7
+ # Tracks created global variables and constants in block.
8
+ class GlobalsTracker
9
+ # @return [Array<Symbol>]
10
+ attr_accessor :caught_variables
11
+
12
+ # @return [Array<[Object, :Symbol]>]
13
+ attr_accessor :caught_constants
14
+
15
+ # @return [Array<String>, nil]
16
+ attr_accessor :constants_location_prefixes
17
+
18
+ # @param [Array<String>, nil] constants_location_prefixes If given, only constants whose location starts with one
19
+ # of the prefixes will be removed. If nil, all constants
20
+ # will be removed.
21
+ def initialize(constants_location_prefixes: nil)
22
+ @caught_variables = []
23
+ @caught_constants = []
24
+ @constants_location_prefixes = constants_location_prefixes
25
+ end
26
+
27
+ # @yieldreturn [T]
28
+ # @return [T]
29
+ def catch
30
+ before_variables = global_variables
31
+ before_global_constants = Object.constants
32
+ before_binding_constants = Bade::Runtime::RenderBinding.constants(false)
33
+
34
+ res = nil
35
+ begin
36
+ res = yield
37
+ ensure
38
+ @caught_variables += global_variables - before_variables
39
+
40
+ @caught_constants += (Object.constants - before_global_constants)
41
+ .map { |name| [Object, name] }
42
+ @caught_constants += (Bade::Runtime::RenderBinding.constants(false) - before_binding_constants)
43
+ .map { |name| [Bade::Runtime::RenderBinding, name] }
44
+ end
45
+
46
+ res
47
+ end
48
+
49
+ def clear_all
50
+ clear_global_variables
51
+ clear_constants
52
+ end
53
+
54
+ def clear_constants
55
+ _filtered_constants.each do |(obj, name)|
56
+ obj.send(:remove_const, name)
57
+ end
58
+ @caught_constants = []
59
+ end
60
+
61
+ def clear_global_variables
62
+ @caught_variables.each do |name|
63
+ eval("#{name} = nil", binding, __FILE__, __LINE__)
64
+ end
65
+ end
66
+
67
+ # Filteres caught constants by location prefixes and returns ones that should be removed.
68
+ #
69
+ # @return [Array<[Object, :Symbol]>]
70
+ def _filtered_constants
71
+ @caught_constants.select do |(obj, name)|
72
+ next unless obj.const_defined?(name)
73
+ next true if constants_location_prefixes.nil?
74
+
75
+ konst = obj.const_get(name)
76
+ begin
77
+ location = Bade.where_is(konst)
78
+ rescue ::ArgumentError
79
+ next
80
+ end
81
+
82
+ path = location.first
83
+ constants_location_prefixes&.any? { |prefix| path.start_with?(prefix) }
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -9,8 +9,8 @@ module Bade
9
9
  class Mixin < Block
10
10
  ruby2_keywords def call!(blocks, *args)
11
11
  begin
12
- block.call(blocks, *args)
13
- rescue ArgumentError => e
12
+ __call(blocks, *args)
13
+ rescue ::ArgumentError => e
14
14
  case e.message
15
15
  when /\Awrong number of arguments \(given ([0-9]+), expected ([0-9]+)\)\Z/,
16
16
  /\Awrong number of arguments \(([0-9]+) for ([0-9]+)\)\Z/
@@ -19,12 +19,19 @@ module Bade
19
19
  # minus one, because first argument is always hash of blocks
20
20
  given = $1.to_i - 1
21
21
  expected = $2.to_i - 1
22
- raise ArgumentError, "wrong number of arguments (given #{given}, expected #{expected}) for mixin `#{name}`"
22
+ msg = "wrong number of arguments (given #{given}, expected #{expected}) for mixin `#{name}`"
23
+ raise Bade::Runtime::ArgumentError.new(msg, render_binding.__location_stack)
23
24
 
24
25
  when /\Aunknown keyword: (.*)\Z/
25
26
  # handle unknown key-value parameter
26
27
  key_name = $1
27
- raise ArgumentError, "unknown key-value argument `#{key_name}` for mixin `#{name}`"
28
+ msg = "unknown key-value argument `#{key_name}` for mixin `#{name}`"
29
+ raise Bade::Runtime::ArgumentError.new(msg, render_binding.__location_stack)
30
+
31
+ when /\Amissing keyword: :?(.*)\Z/
32
+ key_name = $1
33
+ msg = "missing value for required key-value argument `#{key_name}` for mixin `#{name}`"
34
+ raise Bade::Runtime::ArgumentError.new(msg, render_binding.__location_stack)
28
35
 
29
36
  else
30
37
  raise
@@ -36,10 +43,18 @@ module Bade
36
43
  when :render
37
44
  "Mixin `#{name}` requires block to get rendered content of block `#{e.name}`"
38
45
  else
39
- raise ::ArgumentError, "Unknown context #{e.context} of error #{e}!"
46
+ raise Bade::Runtime::ArgumentError.new("Unknown context #{e.context} of error #{e}!",
47
+ render_binding.__location_stack)
40
48
  end
41
49
 
42
- raise Block::MissingBlockDefinitionError.new(e.name, e.context, msg)
50
+ raise Block::MissingBlockDefinitionError.new(e.name, e.context, msg, render_binding.__location_stack)
51
+
52
+ rescue RuntimeError
53
+ raise
54
+
55
+ rescue Exception => e
56
+ msg = "Exception raised during execution of mixin `#{name}`: #{e}"
57
+ raise Bade::Runtime::RuntimeError.wrap_existing_error(msg, e, render_binding.__location_stack)
43
58
  end
44
59
  end
45
60
  end
@@ -5,12 +5,15 @@ require_relative 'block'
5
5
  module Bade
6
6
  module Runtime
7
7
  class RenderBinding
8
- class KeyError < ::StandardError; end
8
+ Location = Bade::Runtime::Location
9
9
 
10
10
  # @return [Array<Array<String>>]
11
11
  #
12
12
  attr_accessor :__buffs_stack
13
13
 
14
+ # @return [Array<Location>]
15
+ attr_accessor :__location_stack
16
+
14
17
  # @return [Hash<String, Mixin>]
15
18
  #
16
19
  attr_accessor :__mixins
@@ -34,13 +37,14 @@ module Bade
34
37
  end
35
38
  end
36
39
 
37
- # Resets this binding to default state, this method should be envoked after running the template lambda
40
+ # Resets this binding to default state, this method should be evoked after running the template lambda
38
41
  #
39
42
  # @return [nil]
40
43
  #
41
44
  def __reset
42
- @__buffs_stack = [[]]
43
- @__mixins = Hash.new { |_hash, key| raise "Undefined mixin '#{key}'" }
45
+ @__buffs_stack = []
46
+ @__location_stack = []
47
+ @__mixins = Hash.new { |_hash, key| raise Bade::Runtime::KeyError.new("Undefined mixin '#{key}'", __location_stack) }
44
48
  end
45
49
 
46
50
  # @return [Binding]
@@ -51,26 +55,56 @@ module Bade
51
55
 
52
56
  # Shortcut for creating blocks
53
57
  #
54
- def __create_block(name, &block)
55
- Bade::Runtime::Block.new(name, self, &block)
58
+ def __create_block(name, location = nil, &block)
59
+ Bade::Runtime::Block.new(name, location, self, &block)
56
60
  end
57
61
 
58
- def __create_mixin(name, &block)
59
- Bade::Runtime::Mixin.new(name, self, &block)
62
+ def __create_mixin(name, location, &block)
63
+ Bade::Runtime::Mixin.new(name, location, self, &block)
60
64
  end
61
65
 
62
- # --- Methods for dealing with pushing and poping buffers in stack
66
+ # --- Methods for dealing with pushing and popping buffers in stack
63
67
 
64
68
  def __buff
65
- __buffs_stack.last
69
+ __buffs_stack.first
66
70
  end
67
71
 
68
- def __buffs_push
69
- __buffs_stack.push([])
72
+ # @param [RenderBinding::Location, nil] location
73
+ def __buffs_push(location)
74
+ __buffs_stack.unshift([])
75
+ __location_stack.unshift(location) unless location.nil?
70
76
  end
71
77
 
78
+ # @return [Array<String>, nil]
72
79
  def __buffs_pop
73
- __buffs_stack.pop
80
+ __location_stack.shift
81
+ __buffs_stack.shift
82
+ end
83
+
84
+ # --- Other internal methods
85
+
86
+ # @param [String] filename
87
+ def __load(filename)
88
+ # FakeFS does not fake `load` method
89
+ if Object.const_defined?(:FakeFS) && Object.const_get(:FakeFS).activated?
90
+ # rubocop:disable Security/Eval
91
+ eval(File.read(filename), __get_binding, filename)
92
+ # rubocop:enable Security/Eval
93
+ else
94
+ load(filename)
95
+ end
96
+ end
97
+
98
+ # @param [String] filename
99
+ def require_relative(filename)
100
+ # FakeFS does not fake `require_relative` method
101
+ if Object.const_defined?(:FakeFS) && Object.const_get(:FakeFS).activated?
102
+ # rubocop:disable Security/Eval
103
+ eval(File.read(filename), __get_binding, filename)
104
+ # rubocop:enable Security/Eval
105
+ else
106
+ Kernel.require_relative(filename)
107
+ end
74
108
  end
75
109
 
76
110
  # Escape input text with html escapes
@@ -94,6 +128,15 @@ module Bade
94
128
 
95
129
  %( #{name}="#{values.join(' ')}")
96
130
  end
131
+
132
+ def __update_lineno(number)
133
+ __location_stack.first&.lineno = number
134
+ end
135
+
136
+ # @return [Location, nil]
137
+ def __current_location
138
+ __location_stack.first
139
+ end
97
140
  end
98
141
  end
99
142
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Inspired by https://gist.github.com/wtaysom/1236979
4
+
5
+ module Bade
6
+ module Where
7
+ class << self
8
+ def is_proc(proc)
9
+ source_location(proc)
10
+ end
11
+
12
+ def is_method(klass, method_name)
13
+ source_location(klass.method(method_name))
14
+ end
15
+
16
+ def is_instance_method(klass, method_name)
17
+ source_location(klass.instance_method(method_name))
18
+ end
19
+
20
+ def are_methods(klass, method_name)
21
+ are_via_extractor(:method, klass, method_name)
22
+ end
23
+
24
+ def are_instance_methods(klass, method_name)
25
+ are_via_extractor(:method, klass, method_name)
26
+ end
27
+
28
+ def is_class(klass)
29
+ defined_methods(klass)
30
+ .group_by { |sl| sl[0] }
31
+ .map do |file, sls|
32
+ lines = sls.map { |sl| sl[1] }
33
+ count = lines.size
34
+ line = lines.min
35
+
36
+ {
37
+ file: file,
38
+ count: count,
39
+ line: line
40
+ }
41
+ end
42
+ .sort_by { |fc| fc[:count] }
43
+ .map { |fc| [fc[:file], fc[:line]] }
44
+ end
45
+
46
+ # Raises ArgumentError if klass does not have any Ruby methods defined in it.
47
+ def is_class_primarily(klass)
48
+ source_locations = is_class(klass)
49
+ if source_locations.empty?
50
+ methods = defined_methods(klass)
51
+ msg = if methods.empty?
52
+ "#{klass} has no methods"
53
+ else
54
+ "#{klass} only has built-in methods (#{methods.size} in total)"
55
+ end
56
+
57
+ raise ::ArgumentError, msg
58
+ end
59
+ source_locations[0]
60
+ end
61
+
62
+ private
63
+
64
+ def source_location(method)
65
+ method.source_location || (
66
+ method.to_s =~ /: (.*)>/
67
+ Regexp.last_match(1)
68
+ )
69
+ end
70
+
71
+ def are_via_extractor(extractor, klass, method_name)
72
+ klass.ancestors
73
+ .map do |ancestor|
74
+ method = ancestor.send(extractor, method_name)
75
+ source_location(method) if method.owner == ancestor
76
+ end
77
+ .compact
78
+ end
79
+
80
+ def defined_methods(klass)
81
+ methods = klass.methods(false).map { |m| klass.method(m) } +
82
+ klass.instance_methods(false).map { |m| klass.instance_method(m) }
83
+ methods
84
+ .map(&:source_location)
85
+ .compact
86
+ end
87
+ end
88
+ end
89
+
90
+ def self.where_is(klass, method = nil)
91
+ if method
92
+ begin
93
+ Where.is_instance_method(klass, method)
94
+ rescue NameError
95
+ Where.is_method(klass, method)
96
+ end
97
+ else
98
+ Where.is_class_primarily(klass)
99
+ end
100
+ end
101
+ end
data/lib/bade/runtime.rb CHANGED
@@ -1,9 +1,116 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Epuber
3
+ module Bade
4
4
  module Runtime
5
+ Location = Struct.new(:path, :lineno, :label, keyword_init: true) do
6
+ def template?
7
+ path == TEMPLATE_FILE_NAME || path&.include?('.bade')
8
+ end
9
+
10
+ def to_s
11
+ "#{path || TEMPLATE_FILE_NAME}:#{lineno}:in `#{label}'"
12
+ end
13
+ end
14
+
15
+ class RuntimeError < ::StandardError
16
+ # @return [Array<Location>]
17
+ #
18
+ attr_reader :template_backtrace
19
+
20
+ # @return [Boolean]
21
+ #
22
+ attr_reader :print_locations_warning
23
+
24
+ # @param [String] msg
25
+ # @param [Array<Location>] template_backtrace
26
+ # @param [Exception, nil] original
27
+ def initialize(msg, template_backtrace = [], original: nil, print_locations_warning: false)
28
+ super(msg)
29
+ @template_backtrace = template_backtrace
30
+ @original = original
31
+ @print_locations_warning = print_locations_warning
32
+ end
33
+
34
+ def message
35
+ if @template_backtrace.empty?
36
+ super
37
+ else
38
+ warning = if print_locations_warning
39
+ <<~TEXT
40
+
41
+ !!! WARNING !!!, filenames and line numbers of functions can be misleading due to using Ruby
42
+ functions in different Bade file. Trust only functions names. Mixins are fine.
43
+
44
+ This will be fixed in https://github.com/epuber-io/bade/issues/32
45
+ TEXT
46
+ else
47
+ ''
48
+ end
49
+
50
+ <<~MSG.rstrip
51
+ #{super}
52
+ template backtrace:
53
+ #{__formatted_backtrace.join("\n")}
54
+ #{warning}
55
+ MSG
56
+ end
57
+ end
58
+
59
+ def cause
60
+ @original
61
+ end
62
+
63
+ # @return [Array<String>]
64
+ def __formatted_backtrace
65
+ bt = @template_backtrace
66
+
67
+ # delete first location if is same as second (can happen when arguments are incorrect)
68
+ last = bt.first
69
+ bt.delete_at(0) if last && bt.length > 1 && last == bt[1]
70
+
71
+ bt.map { |loc| " #{loc}" }
72
+ end
73
+
74
+ # @param [Array<Thread::Backtrace::Location>, nil] locations
75
+ def self.process_locations(locations)
76
+ return [] if locations.nil?
77
+
78
+ # map to Bade's Location
79
+ new_locations = locations.map { |loc| Location.new(path: loc.path, lineno: loc.lineno, label: loc.label) }
80
+
81
+ # find location to use or drop
82
+ index = new_locations.rindex(&:template?)
83
+ return [] if index.nil?
84
+
85
+ # get only locations inside template
86
+ new_locations = new_locations[0...index] || []
87
+
88
+ # filter out not interested locations
89
+ new_locations
90
+ .reject { |loc| loc.path.start_with?(__dir__) }
91
+ .reject { |loc| loc.template? && loc.label.include?('lambda_instance') }
92
+ end
93
+
94
+ # @param [String] msg
95
+ # @param [Exception] error
96
+ # @param [Array<Location>] template_backtrace
97
+ # @return [RuntimeError]
98
+ def self.wrap_existing_error(msg, error, template_backtrace)
99
+ ruby_locs = Bade::Runtime::RuntimeError.process_locations(error.backtrace_locations)
100
+ locs = ruby_locs + template_backtrace
101
+ Bade::Runtime::RuntimeError.new(msg, locs, original: error, print_locations_warning: !ruby_locs.empty?)
102
+ end
103
+ end
104
+
105
+ class KeyError < RuntimeError; end
106
+
107
+ class ArgumentError < RuntimeError; end
108
+
109
+ TEMPLATE_FILE_NAME = '(__template__)'.freeze
110
+
5
111
  require_relative 'runtime/block'
6
112
  require_relative 'runtime/mixin'
7
113
  require_relative 'runtime/render_binding'
114
+ require_relative 'runtime/globals_tracker'
8
115
  end
9
116
  end
data/lib/bade/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bade
4
- VERSION = '0.2.5'.freeze
4
+ VERSION = '0.3.2'.freeze
5
5
  end
data/lib/bade.rb CHANGED
@@ -7,4 +7,5 @@ module Bade
7
7
  require_relative 'bade/generator'
8
8
  require_relative 'bade/renderer'
9
9
  require_relative 'bade/optimizer'
10
+ require_relative 'bade/precompiled'
10
11
  end