bade 0.2.4 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,12 +6,12 @@ module Bade
6
6
 
7
7
  class Parser
8
8
  module TagRegexps
9
- BLOCK_EXPANSION = /\A:\s+/
9
+ BLOCK_EXPANSION = /\A:\s+/.freeze
10
10
  OUTPUT_CODE = LineIndicatorRegexps::OUTPUT_BLOCK
11
- TEXT_START = /\A /
11
+ TEXT_START = /\A /.freeze
12
12
 
13
- PARAMS_ARGS_DELIMITER = /\A\s*,/
14
- PARAMS_END = /\A\s*\)/
13
+ PARAMS_ARGS_DELIMITER = /\A\s*,/.freeze
14
+ PARAMS_END = /\A\s*\)/.freeze
15
15
  end
16
16
 
17
17
  # @param [String] tag tag name
@@ -6,8 +6,8 @@ module Bade
6
6
 
7
7
  class Parser
8
8
  module TextRegexps
9
- INTERPOLATION_START = /(\\)?(&|#)\{/
10
- INTERPOLATION_END = /\A\}/
9
+ INTERPOLATION_START = /(\\)?(&|#)\{/.freeze
10
+ INTERPOLATION_END = /\A\}/.freeze
11
11
  end
12
12
 
13
13
  def parse_text
@@ -64,7 +64,7 @@ module Bade
64
64
 
65
65
  next_line
66
66
 
67
- @line.remove_indent!(text_indent ? text_indent : indent, @tabsize)
67
+ @line.remove_indent!(text_indent || indent, @tabsize)
68
68
 
69
69
  parse_text
70
70
 
data/lib/bade/parser.rb CHANGED
@@ -17,6 +17,8 @@ module Bade
17
17
  attr_reader :error, :file, :line, :lineno, :column
18
18
 
19
19
  def initialize(error, file, line, lineno, column)
20
+ super(error)
21
+
20
22
  @error = error
21
23
  @file = file || '(__TEMPLATE__)'
22
24
  @line = line.to_s
@@ -60,7 +62,7 @@ module Bade
60
62
  @file_path = file_path
61
63
 
62
64
  @tab_re = /\G((?: {#{tabsize}})*) {0,#{tabsize - 1}}\t/
63
- @tab = '\1' + ' ' * tabsize
65
+ @tab = "\1#{' ' * tabsize}"
64
66
 
65
67
  reset
66
68
  end
@@ -106,7 +108,7 @@ module Bade
106
108
  @stacks << @stacks.last.dup while indent >= @stacks.length
107
109
 
108
110
  parent = @stacks[indent].last
109
- node = AST::NodeRegistrator.create(type, @lineno)
111
+ node = AST::NodeRegistrator.create(type, parent, lineno: @lineno, filename: @file_path)
110
112
  parent.children << node
111
113
 
112
114
  node.value = value unless value.nil?
@@ -126,7 +128,9 @@ module Bade
126
128
 
127
129
  def parse_import
128
130
  # TODO: change this to something better
129
- path = eval(@line) # rubocop:disable Lint/Eval
131
+ # rubocop:disable Security/Eval
132
+ path = eval(@line)
133
+ # rubocop:enable Security/Eval
130
134
  append_node(:import, value: path)
131
135
 
132
136
  @dependency_paths << path unless @dependency_paths.include?(path)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
-
3
+ require 'psych'
5
4
 
6
5
  module Bade
7
6
  class Precompiled
@@ -26,7 +25,11 @@ module Bade
26
25
  file
27
26
  end
28
27
 
29
- hash = YAML.load(file)
28
+ hash = if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.0')
29
+ Psych.safe_load(file, filename: file.path, permitted_classes: [Symbol])
30
+ else
31
+ Psych.safe_load(file, [Symbol])
32
+ end
30
33
  raise LoadError, 'YAML file is not in valid format' unless hash.is_a?(Hash)
31
34
 
32
35
  file_path = hash[:source_file_path]
data/lib/bade/renderer.rb CHANGED
@@ -9,7 +9,7 @@ require_relative 'precompiled'
9
9
 
10
10
  module Bade
11
11
  class Renderer
12
- class LoadError < ::RuntimeError
12
+ class LoadError < Bade::Runtime::RuntimeError
13
13
  # @return [String]
14
14
  #
15
15
  attr_reader :loading_path
@@ -22,18 +22,29 @@ module Bade
22
22
  # @param [String] reference_path reference file from which is load performed
23
23
  # @param [String] msg standard message
24
24
  #
25
- def initialize(loading_path, reference_path, msg = nil)
26
- super(msg)
25
+ def initialize(loading_path, reference_path, msg, template_backtrace = [])
26
+ super(msg, template_backtrace)
27
27
  @loading_path = loading_path
28
28
  @reference_path = reference_path
29
29
  end
30
30
  end
31
31
 
32
- def initialize
33
- @optimize = false
32
+ class << self
33
+ def _globals_tracker
34
+ @globals_tracker ||= Bade::Runtime::GlobalsTracker.new
35
+ end
36
+
37
+ # When set to true it will remove all constants that template created. Off by default.
38
+ #
39
+ # @return [Boolean]
40
+ attr_accessor :clear_constants
34
41
  end
35
42
 
36
- TEMPLATE_FILE_NAME = '(__template__)'.freeze
43
+ # @param clear_constants [Boolean] When set to true it will remove all constants that template created. Off by default.
44
+ def initialize(clear_constants: Bade::Renderer.clear_constants)
45
+ @optimize = false
46
+ @clear_constants = clear_constants
47
+ end
37
48
 
38
49
  # @return [String]
39
50
  #
@@ -59,6 +70,10 @@ module Bade
59
70
  #
60
71
  attr_accessor :optimize
61
72
 
73
+ # @return [Boolean] When set to true it will remove all constants that template created. Off by default.
74
+ #
75
+ attr_accessor :clear_constants
76
+
62
77
 
63
78
  # ----------------------------------------------------------------------------- #
64
79
  # Internal attributes
@@ -77,8 +92,8 @@ module Bade
77
92
  #
78
93
  # @return [Renderer] preconfigured instance of this class
79
94
  #
80
- def self.from_source(source, file_path = nil)
81
- inst = new
95
+ def self.from_source(source, file_path = nil, clear_constants: Bade::Renderer.clear_constants)
96
+ inst = new(clear_constants: clear_constants)
82
97
  inst.source_text = source
83
98
  inst.file_path = file_path
84
99
  inst
@@ -88,14 +103,14 @@ module Bade
88
103
  #
89
104
  # @return [Renderer] preconfigured instance of this class
90
105
  #
91
- def self.from_file(file)
106
+ def self.from_file(file, clear_constants: Bade::Renderer.clear_constants)
92
107
  path = if file.is_a?(File)
93
108
  file.path
94
109
  else
95
110
  file
96
111
  end
97
112
 
98
- from_source(nil, path)
113
+ from_source(nil, path, clear_constants: clear_constants)
99
114
  end
100
115
 
101
116
  # Method to create Renderer from Precompiled object, for example when you want to reuse precompiled object from disk
@@ -104,8 +119,8 @@ module Bade
104
119
  #
105
120
  # @return [Renderer] preconfigured instance of this class
106
121
  #
107
- def self.from_precompiled(precompiled)
108
- inst = new
122
+ def self.from_precompiled(precompiled, clear_constants: Bade::Renderer.clear_constants)
123
+ inst = new(clear_constants: clear_constants)
109
124
  inst.precompiled = precompiled
110
125
  inst
111
126
  end
@@ -125,11 +140,20 @@ module Bade
125
140
  self
126
141
  end
127
142
 
143
+ # @return [self]
128
144
  def with_binding(binding)
129
145
  self.lambda_binding = binding
130
146
  self
131
147
  end
132
148
 
149
+ # @param [RenderBinding] binding
150
+ # @return [self]
151
+ def with_render_binding(binding)
152
+ self.lambda_binding = nil
153
+ self.render_binding = binding
154
+ self
155
+ end
156
+
133
157
  def optimized
134
158
  self.optimize = true
135
159
  self
@@ -164,17 +188,21 @@ module Bade
164
188
 
165
189
  # @return [RenderBinding]
166
190
  #
191
+ # rubocop:disable Lint/DuplicateMethods
167
192
  def render_binding
168
193
  @render_binding ||= Runtime::RenderBinding.new(locals || {})
169
194
  end
195
+ # rubocop:enable Lint/DuplicateMethods
170
196
 
171
197
  # @return [Proc]
172
198
  #
173
199
  def lambda_instance
174
- if lambda_binding
175
- lambda_binding.eval(lambda_string, file_path || TEMPLATE_FILE_NAME)
176
- else
177
- render_binding.instance_eval(lambda_string, file_path || TEMPLATE_FILE_NAME)
200
+ _catching_globals do
201
+ if lambda_binding
202
+ lambda_binding.eval(lambda_string, file_path || Bade::Runtime::TEMPLATE_FILE_NAME)
203
+ else
204
+ render_binding.instance_eval(lambda_string, file_path || Bade::Runtime::TEMPLATE_FILE_NAME)
205
+ end
178
206
  end
179
207
  end
180
208
 
@@ -195,13 +223,22 @@ module Bade
195
223
  Generator::NEW_LINE_NAME.to_sym => new_line,
196
224
  Generator::BASE_INDENT_NAME.to_sym => indent,
197
225
  }
198
- run_vars.reject! { |_key, value| value.nil? } # remove nil values
226
+ run_vars.compact! # remove nil values
199
227
 
200
- lambda_instance.call(**run_vars)
228
+ begin
229
+ return _catching_globals do
230
+ lambda_instance.call(**run_vars)
231
+ end
232
+ rescue Bade::Runtime::RuntimeError => e
233
+ raise e
234
+ rescue Exception => e
235
+ msg = "Exception raised during execution of template: #{e}"
236
+ raise Bade::Runtime::RuntimeError.wrap_existing_error(msg, e, render_binding.__location_stack)
237
+ ensure
238
+ self.class._globals_tracker.clear_constants if @clear_constants
239
+ end
201
240
  end
202
241
 
203
-
204
-
205
242
  private
206
243
 
207
244
  # @param [String] content source code of the template
@@ -249,6 +286,7 @@ module Bade
249
286
 
250
287
  if File.exist?(sub_path)
251
288
  return if sub_path.end_with?('.rb') # handled in Generator
289
+
252
290
  sub_path
253
291
  else
254
292
  bade_path = "#{sub_path}.bade"
@@ -272,5 +310,16 @@ module Bade
272
310
  end
273
311
  end
274
312
  end
313
+
314
+ def _catching_globals(&block)
315
+ if @clear_constants
316
+ self.class._globals_tracker.catch(&block)
317
+ else
318
+ block.call
319
+ end
320
+ end
275
321
  end
276
322
  end
323
+
324
+ # Set default value to clear_constants
325
+ Bade::Renderer.clear_constants = false
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ def ruby2_keywords(*) end if RUBY_VERSION < '2.7'
@@ -13,12 +13,10 @@ class String
13
13
  %('#{self}')
14
14
  end
15
15
 
16
-
17
16
  def blank?
18
17
  strip.empty?
19
18
  end
20
19
 
21
-
22
20
  def remove_last(count = 1)
23
21
  slice(0, length - count)
24
22
  end
@@ -27,7 +25,6 @@ class String
27
25
  slice!(length - count, count)
28
26
  end
29
27
 
30
-
31
28
  def remove_first(count = 1)
32
29
  slice(count, length - count)
33
30
  end
@@ -67,7 +64,6 @@ class String
67
64
  remove_first(__chars_count_for_indent(indent, tabsize))
68
65
  end
69
66
 
70
-
71
67
  # Remove indent
72
68
  #
73
69
  # @param [Int] indent
@@ -77,7 +73,6 @@ class String
77
73
  remove_first!(__chars_count_for_indent(indent, tabsize))
78
74
  end
79
75
 
80
-
81
76
  # Calculate indent for line
82
77
  #
83
78
  # @param [Int] tabsize
@@ -88,9 +83,10 @@ class String
88
83
  count = 0
89
84
 
90
85
  each_char do |char|
91
- if char == SPACE_CHAR
86
+ case char
87
+ when SPACE_CHAR
92
88
  count += 1
93
- elsif char == TAB_CHAR
89
+ when TAB_CHAR
94
90
  count += tabsize
95
91
  else
96
92
  break
@@ -105,7 +101,7 @@ class String
105
101
  #
106
102
  def strip_heredoc
107
103
  min_val = scan(/^[ \t]*(?=\S)/).min
108
- indent = (min_val && min_val.size) || 0
104
+ indent = min_val&.size || 0
109
105
  gsub(/^[ \t]{#{indent}}/, '')
110
106
  end
111
107
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../ruby2_keywords'
4
+
3
5
  module Bade
4
6
  module Runtime
5
- class RuntimeError < ::StandardError; end
6
-
7
7
  class Block
8
8
  class MissingBlockDefinitionError < RuntimeError
9
9
  # @return [String]
@@ -14,8 +14,8 @@ module Bade
14
14
  #
15
15
  attr_accessor :context
16
16
 
17
- def initialize(name, context, msg = nil)
18
- super()
17
+ def initialize(name, context, msg, template_backtrace)
18
+ super(msg, template_backtrace)
19
19
 
20
20
  self.name = name
21
21
  self.context = context
@@ -36,34 +36,51 @@ module Bade
36
36
  #
37
37
  attr_reader :name
38
38
 
39
+ # @return [RenderBinding::Location, nil]
40
+ #
41
+ attr_reader :location
42
+
39
43
  # @return [RenderBinding]
40
44
  #
41
45
  attr_reader :render_binding
42
46
 
43
47
  # @param [String] name name of the block
48
+ # @param [RenderBinding::Location, nil] location
44
49
  # @param [RenderBinding] render_binding reference to current binding instance
45
50
  # @param [Proc] block reference to lambda
46
51
  #
47
- def initialize(name, render_binding, &block)
52
+ def initialize(name, location, render_binding, &block)
48
53
  @name = name
54
+ @location = location
49
55
  @render_binding = render_binding
50
56
  @block = block
51
57
  end
52
58
 
53
59
  # --- Calling methods
54
60
 
55
- def call(*args)
61
+ # Calls the block and adds rendered content into current buffer stack.
62
+ #
63
+ # @return [Void]
64
+ ruby2_keywords def call(*args)
56
65
  call!(*args) unless @block.nil?
57
66
  end
58
67
 
59
- def call!(*args)
60
- raise MissingBlockDefinitionError.new(name, :call) if @block.nil?
68
+ # Calls the block and adds rendered content into current buffer stack.
69
+ #
70
+ # @return [Void]
71
+ ruby2_keywords def call!(*args)
72
+ raise MissingBlockDefinitionError.new(name, :call, nil, render_binding.__location_stack) if @block.nil?
61
73
 
62
- render_binding.__buff.concat(@block.call(*args))
74
+ __call(*args)
63
75
  end
64
76
 
65
77
  # --- Rendering methods
66
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]
67
84
  def render(*args)
68
85
  if @block.nil?
69
86
  ''
@@ -72,10 +89,33 @@ module Bade
72
89
  end
73
90
  end
74
91
 
92
+ # Calls the block and returns rendered content in string.
93
+ #
94
+ # Throws error when there is no block.
95
+ #
96
+ # @return [String]
75
97
  def render!(*args)
76
- 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)
77
116
 
78
- @block.call(*args).join
117
+ res = render_binding.__buffs_pop
118
+ render_binding.__buff&.concat(res) if !res.nil? && !res.empty?
79
119
  end
80
120
  end
81
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
@@ -1,43 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../ruby2_keywords'
4
+
3
5
  module Bade
4
6
  module Runtime
5
7
  require_relative 'block'
6
8
 
7
9
  class Mixin < Block
8
- def call!(blocks, *args)
9
- block.call(blocks, *args)
10
- rescue ArgumentError => e
11
- case e.message
12
- when /\Awrong number of arguments \(given ([0-9]+), expected ([0-9]+)\)\Z/,
13
- /\Awrong number of arguments \(([0-9]+) for ([0-9]+)\)\Z/
14
- # handle incorrect parameters count
15
-
16
- # minus one, because first argument is always hash of blocks
17
- given = $1.to_i - 1
18
- expected = $2.to_i - 1
19
- raise ArgumentError, "wrong number of arguments (given #{given}, expected #{expected}) for mixin `#{name}`"
20
-
21
- when /\Aunknown keyword: (.*)\Z/
22
- # handle unknown key-value parameter
23
- key_name = $1
24
- raise ArgumentError, "unknown key-value argument `#{key_name}` for mixin `#{name}`"
25
-
26
- else
27
- raise
28
- end
10
+ ruby2_keywords def call!(blocks, *args)
11
+ begin
12
+ __call(blocks, *args)
13
+ rescue ::ArgumentError => e
14
+ case e.message
15
+ when /\Awrong number of arguments \(given ([0-9]+), expected ([0-9]+)\)\Z/,
16
+ /\Awrong number of arguments \(([0-9]+) for ([0-9]+)\)\Z/
17
+ # handle incorrect parameters count
18
+
19
+ # minus one, because first argument is always hash of blocks
20
+ given = $1.to_i - 1
21
+ expected = $2.to_i - 1
22
+ msg = "wrong number of arguments (given #{given}, expected #{expected}) for mixin `#{name}`"
23
+ raise Bade::Runtime::ArgumentError.new(msg, render_binding.__location_stack)
24
+
25
+ when /\Aunknown keyword: (.*)\Z/
26
+ # handle unknown key-value parameter
27
+ key_name = $1
28
+ msg = "unknown key-value argument `#{key_name}` for mixin `#{name}`"
29
+ raise Bade::Runtime::ArgumentError.new(msg, render_binding.__location_stack)
29
30
 
30
- rescue Block::MissingBlockDefinitionError => e
31
- msg = case e.context
32
- when :call
33
- "Mixin `#{name}` requires block to get called of block `#{e.name}`"
34
- when :render
35
- "Mixin `#{name}` requires block to get rendered content of block `#{e.name}`"
36
- else
37
- raise ::ArgumentError, "Unknown context #{e.context} of error #{e}!"
38
- end
39
-
40
- raise Block::MissingBlockDefinitionError.new(e.name, e.context, msg)
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)
35
+
36
+ else
37
+ raise
38
+ end
39
+ rescue Block::MissingBlockDefinitionError => e
40
+ msg = case e.context
41
+ when :call
42
+ "Mixin `#{name}` requires block to get called of block `#{e.name}`"
43
+ when :render
44
+ "Mixin `#{name}` requires block to get rendered content of block `#{e.name}`"
45
+ else
46
+ raise Bade::Runtime::ArgumentError.new("Unknown context #{e.context} of error #{e}!",
47
+ render_binding.__location_stack)
48
+ end
49
+
50
+ raise Block::MissingBlockDefinitionError.new(e.name, e.context, msg, render_binding.__location_stack)
51
+
52
+ rescue Exception => e
53
+ msg = "Exception raised during execution of mixin `#{name}`: #{e}"
54
+ raise Bade::Runtime::RuntimeError.wrap_existing_error(msg, e, render_binding.__location_stack)
55
+ end
41
56
  end
42
57
  end
43
58
  end