bade 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: []