bade 0.2.5 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 148aef838267196453c729800e51c4d3b1f194586f5226e00c659d8f3fc80e58
4
- data.tar.gz: dff1e493ea8a08976cd6cdd1fc80cf937cb2582bc41630c2ca6198fc9eab5ada
3
+ metadata.gz: 202b283306b0a05d870766ff8d7c17f8312727843632958adb2bb3f2300a673f
4
+ data.tar.gz: '0820f4f77eb7936b77748257faa1fd4ee51b206d4a7929c656f717127c7b2248'
5
5
  SHA512:
6
- metadata.gz: 86853f78cc0d8647ab5d639664dbb12d5e7101165641d91e4afdbc2affb3cf2c5434ea85bc7f9f92cec7ed571d34a4f23dd4bc0db6eaea9522df29e986df071f
7
- data.tar.gz: 335be41aa317132979197275b9c6fad798525af1247d2878239229e4f81b6beaac916000af24a1da0ccd61ea22ee3356127aa7a485fe806cc9385ef8945370be
6
+ metadata.gz: ecd2dbf7aa570c8051a2e1b59a1a537583cd2cd5005bb06328795de099fab4213a9cfd616a788d66e333a862253d6aecedd928632e3edb923f021e86658f6780
7
+ data.tar.gz: 8633aba2120eaf8f60f96a357cde8eda8c32d862e92dca3c060f66c014bf1031e5bc2916077ee1d1a7de6494cc62c426ded062065c664949acaf849fce71de66
data/Bade.gemspec CHANGED
@@ -14,16 +14,17 @@ Gem::Specification.new do |spec|
14
14
  spec.summary = 'Minimalistic template engine for Ruby.'
15
15
  spec.homepage = 'https://github.com/epuber-io/bade'
16
16
  spec.license = 'MIT'
17
- spec.required_ruby_version = '>= 2.0'
17
+ spec.metadata = { 'rubygems_mfa_required' => 'true' }
18
+ spec.required_ruby_version = '>= 2.5'
18
19
 
19
20
  spec.files = Dir['bin/**/*'] + Dir['lib/**/*'] + %w[Bade.gemspec Gemfile README.md]
20
21
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
22
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
23
  spec.require_paths = ['lib']
23
24
 
24
- spec.add_dependency 'psych', '>= 2.2', '< 4.0'
25
+ spec.add_dependency 'psych', '>= 2.2', '< 5.0'
25
26
 
26
- spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'fakefs', '~> 1.3'
27
28
  spec.add_development_dependency 'rspec', '~> 3.2'
28
- spec.add_development_dependency 'rubocop', '~> 0.50.0'
29
+ spec.add_development_dependency 'rubocop', '~> 1.14'
29
30
  end
data/Gemfile CHANGED
@@ -5,9 +5,13 @@ source 'https://rubygems.org'
5
5
  gemspec
6
6
 
7
7
  gem 'benchmark-ips', require: false
8
- gem 'coveralls', require: false
9
8
 
10
- group :banchmarks, optional: true do
9
+ group :dev, optional: true do
10
+ gem 'debase', require: false
11
+ gem 'ruby-debug-ide', require: false
12
+ end
13
+
14
+ group :benchmarks, optional: true do
11
15
  gem 'flamegraph'
12
16
  gem 'ruby-prof'
13
17
  end
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  # Bade
3
3
 
4
- [![Gem Version](https://badge.fury.io/rb/bade.svg)](http://badge.fury.io/rb/bade) [![Build Status](https://travis-ci.org/epuber-io/bade.svg?branch=master)](https://travis-ci.org/epuber-io/bade) [![Coverage Status](https://coveralls.io/repos/epuber-io/bade/badge.svg?branch=master&service=github)](https://coveralls.io/github/epuber-io/bade?branch=master) [![Inline docs](http://inch-ci.org/github/epuber-io/bade.svg?branch=master)](http://inch-ci.org/github/epuber-io/bade)
4
+ [![Gem Version](https://badge.fury.io/rb/bade.svg)](http://badge.fury.io/rb/bade) [![Build Status](https://github.com/epuber-io/bade/actions/workflows/tests.yml/badge.svg)](https://github.com/epuber-io/bade/actions) [![Coverage Status](https://coveralls.io/repos/epuber-io/bade/badge.svg?branch=master&service=github)](https://coveralls.io/github/epuber-io/bade?branch=master) [![Inline docs](https://inch-ci.org/github/epuber-io/bade.svg?branch=master)](https://inch-ci.org/github/epuber-io/bade)
5
5
 
6
6
 
7
7
  Minimalistic template engine written in Ruby for Ruby. Developed mainly to help with creating e-books. Highly influenced by [Jade](http://jade-lang.com) and [Slim](http://slim-lang.com).
@@ -30,7 +30,7 @@ Or install it yourself as:
30
30
 
31
31
  ## Development
32
32
 
33
- After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests.
33
+ After checking out the repo, run `bundle install --with dev` to install dependencies. Then, run `rake spec` to run the tests.
34
34
 
35
35
  To install this gem onto your local machine, run `bundle exec rake install`.
36
36
 
@@ -40,7 +40,7 @@ To install this gem onto your local machine, run `bundle exec rake install`.
40
40
  Bug reports and pull requests are welcome on GitHub at https://github.com/epuber-io/bade. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
41
41
 
42
42
 
43
- ## TODO
43
+ ## TODO
44
44
 
45
45
  - [ ] create documentation about syntax
46
46
  - [ ] create several examples
@@ -24,7 +24,7 @@ module Bade
24
24
 
25
25
  # @param root [Bade::Node]
26
26
  #
27
- def initialize(root: Node.new(:root), file_path: nil)
27
+ def initialize(root: Node.new(:root, nil), file_path: nil)
28
28
  @root = root
29
29
 
30
30
  @file_path = file_path.dup.freeze unless file_path.nil?
@@ -17,17 +17,26 @@ module Bade
17
17
  #
18
18
  attr_accessor :conditional
19
19
 
20
+ # @return [String, nil]
21
+ #
22
+ attr_accessor :default_value
23
+
20
24
  ruby2_keywords def initialize(*args)
21
25
  super(*args)
22
26
 
23
27
  @escaped = false
24
28
  @conditional = false
29
+ @default_value = nil
25
30
  end
26
31
 
27
32
  # @param [ValueNode] other
28
33
  #
29
34
  def ==(other)
30
- super && value == other.value && escaped == other.escaped && conditional == other.conditional
35
+ super &&
36
+ value == other.value &&
37
+ escaped == other.escaped &&
38
+ conditional == other.conditional &&
39
+ default_value == other.default_value
31
40
  end
32
41
  end
33
42
  end
data/lib/bade/ast/node.rb CHANGED
@@ -10,7 +10,11 @@ module Bade
10
10
  #
11
11
  attr_reader :type
12
12
 
13
- # @return [Array<Bade::Node>]
13
+ # @return [Bade::AST::Node, nil]
14
+ #
15
+ attr_accessor :parent
16
+
17
+ # @return [Array<Bade::AST::Node>]
14
18
  #
15
19
  attr_accessor :children
16
20
 
@@ -20,10 +24,16 @@ module Bade
20
24
  #
21
25
  attr_reader :lineno
22
26
 
23
- def initialize(type, lineno: nil)
27
+ # @return [String] filename
28
+ #
29
+ attr_reader :filename
30
+
31
+ def initialize(type, parent = nil, lineno: nil, filename: nil)
24
32
  @type = type
33
+ @parent = parent
25
34
  @children = []
26
35
  @lineno = lineno
36
+ @filename = filename
27
37
  end
28
38
 
29
39
  def to_s
@@ -38,12 +38,12 @@ module Bade
38
38
  #
39
39
  # @return [Bade::AST::Node]
40
40
  #
41
- def create(type, lineno)
41
+ def create(type, parent, lineno: nil, filename: nil)
42
42
  klass = registered_types[type]
43
43
 
44
44
  raise ::KeyError, "Undefined node type #{type.inspect}" if klass.nil?
45
45
 
46
- klass.new(type, lineno: lineno)
46
+ klass.new(type, parent, lineno: lineno, filename: filename)
47
47
  end
48
48
  end
49
49
 
@@ -55,6 +55,7 @@ module Bade
55
55
  register_type DoctypeNode, :doctype
56
56
 
57
57
  register_type ValueNode, :import
58
+ register_type ValueNode, :yield
58
59
 
59
60
  # --- Comments
60
61
 
@@ -37,13 +37,13 @@ module Bade
37
37
 
38
38
  children_s = ''
39
39
  if node.children.count > 0
40
- children_s = "\n" + node.children.map { |n| node_to_s(n, level + 1) }.join("\n") + "\n#{indent}"
40
+ children_s = "\n#{node.children.map { |n| node_to_s(n, level + 1) }.join("\n")}\n#{indent}"
41
41
  end
42
42
 
43
43
  other = ''
44
44
 
45
45
  case node
46
- when TagNode
46
+ when TagNode, MixinCommonNode
47
47
  other = node.name
48
48
  when KeyValueNode
49
49
  other = "#{node.name}:#{node.value}"
@@ -56,15 +56,13 @@ module Bade
56
56
  ''
57
57
  end
58
58
  other = "#{escaped_sign}#{node.value}"
59
- when MixinCommonNode
60
- other = node.name
61
59
  when Node
62
60
  # nothing
63
61
  else
64
62
  raise "Unknown node class #{node.class} of type #{node.type} for serializing"
65
63
  end
66
64
 
67
- other = ' ' + other if other && !other.empty?
65
+ other = " #{other}" if other && !other.empty?
68
66
 
69
67
  "#{indent}(#{type_s}#{other}#{children_s})"
70
68
  end
@@ -75,7 +75,7 @@ module Bade
75
75
  end
76
76
 
77
77
  def buff_code(text)
78
- @buff << ' ' * @code_indent + text
78
+ @buff << "#{' ' * @code_indent}#{text}"
79
79
  end
80
80
 
81
81
  # @param document [Bade::Document]
@@ -86,6 +86,7 @@ module Bade
86
86
  end
87
87
 
88
88
  buff_code("# ----- start file #{document.file_path}") unless document.file_path.nil?
89
+ buff_code "__buffs_push(#{location(filename: document.file_path, lineno: 0, label: '<top>')})"
89
90
 
90
91
  new_root = if @optimize
91
92
  Optimizer.new(document.root).optimize
@@ -115,6 +116,8 @@ module Bade
115
116
  # @param current_node [Node]
116
117
  #
117
118
  def visit_node(current_node)
119
+ update_location_node(current_node)
120
+
118
121
  case current_node.type
119
122
  when :root
120
123
  visit_node_children(current_node)
@@ -134,7 +137,7 @@ module Bade
134
137
  buff_print_text ' -->'
135
138
 
136
139
  when :comment
137
- comment_text = '#' + current_node.children.map(&:value).join("\n#")
140
+ comment_text = "##{current_node.children.map(&:value).join("\n#")}"
138
141
  buff_code(comment_text)
139
142
 
140
143
  when :doctype
@@ -166,8 +169,11 @@ module Bade
166
169
  "#{base_path}.rb"
167
170
  end
168
171
 
169
- buff_code "load('#{load_path}')" unless load_path.nil?
170
-
172
+ buff_code "__load('#{load_path}')" unless load_path.nil?
173
+ when :yield
174
+ block_name = DEFAULT_BLOCK_NAME
175
+ method = current_node.conditional ? 'call' : 'call!'
176
+ buff_code "#{block_name}.#{method}"
171
177
  else
172
178
  raise "Unknown type #{current_node.type}"
173
179
  end
@@ -267,19 +273,22 @@ module Bade
267
273
  params = mixin_node.params
268
274
  result = []
269
275
 
270
- if mixin_node.type == :mixin_call
276
+ case mixin_node.type
277
+ when :mixin_call
271
278
  blocks = mixin_node.blocks
272
279
 
273
280
  other_children = (mixin_node.children - mixin_node.blocks - mixin_node.params)
274
281
  if other_children.count { |n| n.type != :newline } > 0
275
- def_block_node = AST::NodeRegistrator.create(:mixin_block, mixin_node.lineno)
282
+ def_block_node = AST::NodeRegistrator.create(:mixin_block, mixin_node, lineno: mixin_node.lineno)
276
283
  def_block_node.name = DEFAULT_BLOCK_NAME
277
284
  def_block_node.children = other_children
278
285
 
279
286
  blocks << def_block_node
280
287
  end
281
288
 
282
- if !blocks.empty?
289
+ if blocks.empty?
290
+ result << '{}'
291
+ else
283
292
  buff_code '__blocks = {}'
284
293
 
285
294
  blocks.each do |block|
@@ -287,17 +296,18 @@ module Bade
287
296
  end
288
297
 
289
298
  result << '__blocks.dup'
290
- else
291
- result << '{}'
292
299
  end
293
- elsif mixin_node.type == :mixin_decl
300
+ when :mixin_decl
294
301
  result << '__blocks'
295
302
  end
296
303
 
304
+ # positional params
305
+ result += params.select { |n| n.type == :mixin_param }
306
+ .map { |param| param.default_value ? "#{param.value} = #{param.default_value}" : param.value }
297
307
 
298
- # normal params
299
- result += params.select { |n| n.type == :mixin_param }.map(&:value)
300
- result += params.select { |n| n.type == :mixin_key_param }.map { |param| "#{param.name}: #{param.value}" }
308
+ # key-value params
309
+ result += params.select { |n| n.type == :mixin_key_param }
310
+ .map { |param| "#{param.name}: #{param.value}" }
301
311
 
302
312
  result.join(', ')
303
313
  end
@@ -309,14 +319,10 @@ module Bade
309
319
  # @return [nil]
310
320
  #
311
321
  def block_definition(block_node)
312
- buff_code "__blocks['#{block_node.name}'] = __create_block('#{block_node.name}') do"
322
+ buff_code "__blocks['#{block_node.name}'] = __create_block('#{block_node.name}', #{location_node(block_node)}) do"
313
323
 
314
324
  code_indent do
315
- buff_code '__buffs_push()'
316
-
317
325
  visit_node_children(block_node)
318
-
319
- buff_code '__buffs_pop()'
320
326
  end
321
327
 
322
328
  buff_code 'end'
@@ -350,7 +356,7 @@ module Bade
350
356
  #
351
357
  def visit_block_decl(current_node)
352
358
  params = formatted_mixin_params(current_node)
353
- buff_code "#{MIXINS_NAME}['#{current_node.name}'] = __create_mixin('#{current_node.name}', &lambda { |#{params}|"
359
+ buff_code "#{MIXINS_NAME}['#{current_node.name}'] = __create_mixin('#{current_node.name}', #{location_node(current_node)}, &lambda { |#{params}|"
354
360
 
355
361
  code_indent do
356
362
  blocks_name_declaration(current_node)
@@ -367,6 +373,54 @@ module Bade
367
373
  def escape_double_quotes!(str)
368
374
  str.gsub!(/"/, '\"')
369
375
  end
376
+
377
+ # @param [Bade::AST::Node]
378
+ # @return [Void]
379
+ def update_location_node(node)
380
+ should_skip = case node.type
381
+ when :code
382
+ value = node.value.strip
383
+
384
+ %w[end else }].include?(value) || value.match(/^when /)
385
+ when :newline
386
+ true
387
+ else
388
+ false
389
+ end
390
+ return if should_skip
391
+ return if node.lineno.nil?
392
+
393
+ buff_code "__update_lineno(#{node.lineno})"
394
+ end
395
+
396
+ # @param [String] filename
397
+ # @param [Fixnum] lineno
398
+ # @param [String] label
399
+ # @return [String]
400
+ def location(filename:, lineno:, label:)
401
+ args = [
402
+ filename ? "path: '#{filename}'" : nil,
403
+ "lineno: #{lineno}",
404
+ "label: '#{label}'",
405
+ ].compact
406
+
407
+ "Location.new(#{args.join(',')})"
408
+ end
409
+
410
+ # @param [Node] node
411
+ # @return [String]
412
+ def location_node(node)
413
+ label = case node.type
414
+ when :mixin_decl
415
+ "+#{node.name}"
416
+ when :mixin_block
417
+ "#{node.name} in +#{node.parent.name}"
418
+ else
419
+ node.name
420
+ end
421
+
422
+ location(filename: node.filename, lineno: node.lineno, label: label)
423
+ end
370
424
  end
371
425
 
372
426
  # backward compatibility
@@ -7,6 +7,7 @@ module Bade
7
7
  class Parser
8
8
  module LineIndicatorRegexps
9
9
  IMPORT = /\Aimport /.freeze
10
+ YIELD = /\Ayield(!?)/.freeze
10
11
  MIXIN_DECL = /\Amixin #{NAME_RE_STRING}/.freeze
11
12
  MIXIN_CALL = /\A\+#{NAME_RE_STRING}/.freeze
12
13
  BLOCK_DECLARATION = /\Ablock #{NAME_RE_STRING}/.freeze
@@ -135,6 +136,11 @@ module Bade
135
136
  @line = $'
136
137
  parse_import
137
138
 
139
+ when LineIndicatorRegexps::YIELD
140
+ @line = $'
141
+ node = append_node(:yield, add: true)
142
+ node.conditional = $1.nil?
143
+
138
144
  when LineIndicatorRegexps::MIXIN_DECL
139
145
  # Mixin declaration
140
146
  @line = $'
@@ -195,12 +201,8 @@ module Bade
195
201
  @line = $' if $1
196
202
  parse_tag($&)
197
203
 
198
- when LineIndicatorRegexps::TAG_CLASS_START_BLOCK
199
- # Found class name -> implicit div
200
- parse_tag 'div'
201
-
202
- when LineIndicatorRegexps::TAG_ID_START_BLOCK
203
- # Found id name -> implicit div
204
+ when LineIndicatorRegexps::TAG_CLASS_START_BLOCK, LineIndicatorRegexps::TAG_ID_START_BLOCK
205
+ # Found a class or id selector.
204
206
  parse_tag 'div'
205
207
 
206
208
  else
@@ -18,6 +18,7 @@ module Bade
18
18
  PARAMS_PARAM_NAME = /\A\s*#{NAME_RE_STRING}/.freeze
19
19
  PARAMS_BLOCK_NAME = /\A\s*&#{NAME_RE_STRING}/.freeze
20
20
  PARAMS_KEY_PARAM_NAME = CODE_ATTR_RE
21
+ PARAMS_PARAM_DEFAULT_START = /\A\s*=/.freeze
21
22
  end
22
23
 
23
24
  def parse_mixin_call(mixin_name)
@@ -114,7 +115,12 @@ module Bade
114
115
 
115
116
  when MixinRegexps::PARAMS_PARAM_NAME
116
117
  @line = $'
117
- append_node(:mixin_param, value: $1)
118
+ attr_node = append_node(:mixin_param, value: $1)
119
+
120
+ if @line =~ MixinRegexps::PARAMS_PARAM_DEFAULT_START
121
+ @line = $'
122
+ attr_node.default_value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG)
123
+ end
118
124
 
119
125
  when MixinRegexps::PARAMS_BLOCK_NAME
120
126
  @line = $'
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?
@@ -26,9 +26,9 @@ module Bade
26
26
  end
27
27
 
28
28
  hash = if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.0')
29
- YAML.safe_load(file, filename: file.path, permitted_classes: [Symbol])
29
+ Psych.safe_load(file, filename: file.path, permitted_classes: [Symbol])
30
30
  else
31
- YAML.safe_load(file, [Symbol])
31
+ Psych.safe_load(file, [Symbol])
32
32
  end
33
33
  raise LoadError, 'YAML file is not in valid format' unless hash.is_a?(Hash)
34
34
 
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
@@ -173,10 +197,12 @@ module Bade
173
197
  # @return [Proc]
174
198
  #
175
199
  def lambda_instance
176
- if lambda_binding
177
- lambda_binding.eval(lambda_string, file_path || TEMPLATE_FILE_NAME)
178
- else
179
- 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
180
206
  end
181
207
  end
182
208
 
@@ -197,13 +223,22 @@ module Bade
197
223
  Generator::NEW_LINE_NAME.to_sym => new_line,
198
224
  Generator::BASE_INDENT_NAME.to_sym => indent,
199
225
  }
200
- run_vars.reject! { |_key, value| value.nil? } # remove nil values
226
+ run_vars.compact! # remove nil values
201
227
 
202
- 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
203
240
  end
204
241
 
205
-
206
-
207
242
  private
208
243
 
209
244
  # @param [String] content source code of the template
@@ -275,5 +310,16 @@ module Bade
275
310
  end
276
311
  end
277
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
278
321
  end
279
322
  end
323
+
324
+ # Set default value to clear_constants
325
+ Bade::Renderer.clear_constants = false
@@ -1,2 +1,3 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  def ruby2_keywords(*) end if RUBY_VERSION < '2.7'
@@ -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
@@ -100,7 +101,7 @@ class String
100
101
  #
101
102
  def strip_heredoc
102
103
  min_val = scan(/^[ \t]*(?=\S)/).min
103
- indent = (min_val && min_val.size) || 0
104
+ indent = min_val&.size || 0
104
105
  gsub(/^[ \t]{#{indent}}/, '')
105
106
  end
106
107
  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,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bade
4
+ module Runtime
5
+ # Tracks created global variables and constants in block.
6
+ class GlobalsTracker
7
+ # @return [Array<Symbol>]
8
+ attr_accessor :caught_variables
9
+
10
+ # @return [Array<[Object, :Symbol]>]
11
+ attr_accessor :caught_constants
12
+
13
+ def initialize
14
+ @caught_variables = []
15
+ @caught_constants = []
16
+ end
17
+
18
+ # @yieldreturn [T]
19
+ # @return [T]
20
+ def catch
21
+ before_variables = global_variables
22
+ before_global_constants = Object.constants
23
+ before_binding_constants = Bade::Runtime::RenderBinding.constants(false)
24
+
25
+ res = nil
26
+ begin
27
+ res = yield
28
+ ensure
29
+ @caught_variables += global_variables - before_variables
30
+
31
+ @caught_constants += (Object.constants - before_global_constants)
32
+ .map { |name| [Object, name] }
33
+ @caught_constants += (Bade::Runtime::RenderBinding.constants(false) - before_binding_constants)
34
+ .map { |name| [Bade::Runtime::RenderBinding, name] }
35
+ end
36
+
37
+ res
38
+ end
39
+
40
+ def clear_all
41
+ clear_global_variables
42
+ clear_constants
43
+ end
44
+
45
+ def clear_constants
46
+ @caught_constants.each do |(obj, name)|
47
+ obj.send(:remove_const, name) if obj.const_defined?(name)
48
+ end
49
+ @caught_constants = []
50
+ end
51
+
52
+ def clear_global_variables
53
+ @caught_variables.each do |name|
54
+ eval("#{name} = nil", binding, __FILE__, __LINE__)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ 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,15 @@ 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 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)
43
55
  end
44
56
  end
45
57
  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 defined?(:FakeFS) && 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 defined?(:FakeFS) && 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
data/lib/bade/runtime.rb CHANGED
@@ -1,9 +1,87 @@
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 to_s
7
+ "#{path || TEMPLATE_FILE_NAME}:#{lineno}:in `#{label}'"
8
+ end
9
+ end
10
+
11
+ class RuntimeError < ::StandardError
12
+ # @return [Array<Location>]
13
+ #
14
+ attr_reader :template_backtrace
15
+
16
+ # @param [String] msg
17
+ # @param [Array<Location>] template_backtrace
18
+ # @param [Exception, nil] original
19
+ def initialize(msg, template_backtrace = [], original: nil)
20
+ super(msg)
21
+ @template_backtrace = template_backtrace
22
+ @original = original
23
+ end
24
+
25
+ def message
26
+ if @template_backtrace.empty?
27
+ super
28
+ else
29
+ <<~MSG.rstrip
30
+ #{super}
31
+ template backtrace:
32
+ #{__formatted_backtrace.join("\n")}
33
+ MSG
34
+ end
35
+ end
36
+
37
+ def cause
38
+ @original
39
+ end
40
+
41
+ # @return [Array<String>]
42
+ def __formatted_backtrace
43
+ bt = @template_backtrace
44
+
45
+ # delete first location if is same as second (can happen when arguments are incorrect)
46
+ last = bt.first
47
+ bt.delete_at(0) if last && bt.length > 1 && last == bt[1]
48
+
49
+ bt.map { |loc| " #{loc}" }
50
+ end
51
+
52
+ # @param [Array<Thread::Backtrace::Location>, nil] locations
53
+ def self.process_locations(locations)
54
+ return [] if locations.nil?
55
+
56
+ index = locations&.find_index { |loc| loc.path == TEMPLATE_FILE_NAME || loc.path&.include?('.bade') }
57
+ return [] if index.nil?
58
+
59
+ new_locations = locations[0...index] || []
60
+
61
+ new_locations.map do |loc|
62
+ Location.new(path: loc.path, lineno: loc.lineno, label: loc.label)
63
+ end
64
+ end
65
+
66
+ # @param [String] msg
67
+ # @param [Exception] error
68
+ # @param [Array<Location>] template_backtrace
69
+ # @return [RuntimeError]
70
+ def self.wrap_existing_error(msg, error, template_backtrace)
71
+ locs = Bade::Runtime::RuntimeError.process_locations(error.backtrace_locations) + template_backtrace
72
+ Bade::Runtime::RuntimeError.new(msg, locs, original: error)
73
+ end
74
+ end
75
+
76
+ class KeyError < RuntimeError; end
77
+
78
+ class ArgumentError < RuntimeError; end
79
+
80
+ TEMPLATE_FILE_NAME = '(__template__)'.freeze
81
+
5
82
  require_relative 'runtime/block'
6
83
  require_relative 'runtime/mixin'
7
84
  require_relative 'runtime/render_binding'
85
+ require_relative 'runtime/globals_tracker'
8
86
  end
9
87
  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.0'.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
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bade
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Kříž
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-25 00:00:00.000000000 Z
11
+ date: 2022-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: psych
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '2.2'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '4.0'
22
+ version: '5.0'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,21 +29,21 @@ dependencies:
29
29
  version: '2.2'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '4.0'
32
+ version: '5.0'
33
33
  - !ruby/object:Gem::Dependency
34
- name: rake
34
+ name: fakefs
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0'
39
+ version: '1.3'
40
40
  type: :development
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0'
46
+ version: '1.3'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rspec
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -64,15 +64,15 @@ dependencies:
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: 0.50.0
67
+ version: '1.14'
68
68
  type: :development
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: 0.50.0
75
- description:
74
+ version: '1.14'
75
+ description:
76
76
  email:
77
77
  - samnung@gmail.com
78
78
  executables: []
@@ -109,14 +109,16 @@ files:
109
109
  - lib/bade/ruby_extensions/string.rb
110
110
  - lib/bade/runtime.rb
111
111
  - lib/bade/runtime/block.rb
112
+ - lib/bade/runtime/globals_tracker.rb
112
113
  - lib/bade/runtime/mixin.rb
113
114
  - lib/bade/runtime/render_binding.rb
114
115
  - lib/bade/version.rb
115
116
  homepage: https://github.com/epuber-io/bade
116
117
  licenses:
117
118
  - MIT
118
- metadata: {}
119
- post_install_message:
119
+ metadata:
120
+ rubygems_mfa_required: 'true'
121
+ post_install_message:
120
122
  rdoc_options: []
121
123
  require_paths:
122
124
  - lib
@@ -124,15 +126,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
126
  requirements:
125
127
  - - ">="
126
128
  - !ruby/object:Gem::Version
127
- version: '2.0'
129
+ version: '2.5'
128
130
  required_rubygems_version: !ruby/object:Gem::Requirement
129
131
  requirements:
130
132
  - - ">="
131
133
  - !ruby/object:Gem::Version
132
134
  version: '0'
133
135
  requirements: []
134
- rubygems_version: 3.1.2
135
- signing_key:
136
+ rubygems_version: 3.3.8
137
+ signing_key:
136
138
  specification_version: 4
137
139
  summary: Minimalistic template engine for Ruby.
138
140
  test_files: []