bade 0.1.4 → 0.2.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.
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+
6
+ module Bade
7
+ class Precompiled
8
+ # @return [String]
9
+ #
10
+ attr_accessor :code_string
11
+
12
+ # @return [String]
13
+ #
14
+ attr_accessor :source_file_path
15
+
16
+ # # @return [Proc]
17
+ # #
18
+ # attr_accessor :lambda_instance
19
+
20
+ # @param [String, File] file file instance or path to file
21
+ #
22
+ def self.from_yaml_file(file)
23
+ file = if file.is_a?(String)
24
+ File.new(file, 'r')
25
+ else
26
+ file
27
+ end
28
+
29
+ hash = YAML.load(file)
30
+ file_path = hash[:source_file_path]
31
+ content = hash[:code_string]
32
+
33
+ new(content, file_path)
34
+ end
35
+
36
+ # @param [String] code
37
+ #
38
+ def initialize(code, source_file_path = nil)
39
+ @code_string = code
40
+ @source_file_path = source_file_path
41
+ end
42
+
43
+ # @param [String, File] file file instance or path to file
44
+ #
45
+ def write_yaml_to_file(file)
46
+ file = if file.is_a?(String)
47
+ File.new(file, 'w')
48
+ else
49
+ file
50
+ end
51
+
52
+ content = {
53
+ source_file_path: source_file_path,
54
+ code_string: code_string,
55
+ }.to_yaml
56
+
57
+ file.write(content)
58
+ file.flush
59
+ end
60
+ end
61
+ end
data/lib/bade/renderer.rb CHANGED
@@ -1,12 +1,36 @@
1
+ # frozen_string_literal: true
1
2
 
2
- require_relative 'node'
3
+ require 'pathname'
3
4
  require_relative 'parser'
4
5
  require_relative 'generator'
5
6
  require_relative 'runtime'
7
+ require_relative 'precompiled'
6
8
 
7
9
 
8
10
  module Bade
9
11
  class Renderer
12
+ class LoadError < ::LoadError
13
+ # @return [String]
14
+ #
15
+ attr_reader :loading_path
16
+
17
+ # @return [String]
18
+ #
19
+ attr_reader :reference_path
20
+
21
+ # @param [String] loading_path currently loaded path
22
+ # @param [String] reference_path reference file from which is load performed
23
+ # @param [String] msg standard message
24
+ #
25
+ def initialize(loading_path, reference_path, msg=nil)
26
+ super(msg)
27
+ @loading_path = loading_path
28
+ @reference_path = reference_path
29
+ end
30
+ end
31
+
32
+ TEMPLATE_FILE_NAME = '(__template__)'
33
+
10
34
  # @return [String]
11
35
  #
12
36
  attr_accessor :source_text
@@ -19,9 +43,31 @@ module Bade
19
43
  #
20
44
  attr_accessor :locals
21
45
 
22
- # @param source [String]
46
+ # @return [Binding]
23
47
  #
24
- # @return [self]
48
+ attr_accessor :lambda_binding
49
+
50
+ # @return [RenderBinding]
51
+ #
52
+ attr_accessor :render_binding
53
+
54
+
55
+ # ----------------------------------------------------------------------------- #
56
+ # Internal attributes
57
+
58
+ # @return [Hash<String, Document>] absolute path => document
59
+ #
60
+ def parsed_documents
61
+ @parsed_documents ||= {}
62
+ end
63
+
64
+
65
+ # ----------------------------------------------------------------------------- #
66
+ # Factory methods
67
+
68
+ # @param [String] source source string that should be parsed
69
+ #
70
+ # @return [Renderer] preconfigured instance of this class
25
71
  #
26
72
  def self.from_source(source, file_path = nil)
27
73
  inst = new
@@ -30,9 +76,9 @@ module Bade
30
76
  inst
31
77
  end
32
78
 
33
- # @param file [String, File]
79
+ # @param [String, File] file file path or file instance, file that should be loaded and parsed
34
80
  #
35
- # @return [self]
81
+ # @return [Renderer] preconfigured instance of this class
36
82
  #
37
83
  def self.from_file(file)
38
84
  path = if file.is_a?(File)
@@ -44,13 +90,18 @@ module Bade
44
90
  from_source(nil, path)
45
91
  end
46
92
 
47
-
48
- def initialize
49
- # absolute path => document
50
- @parsed_documents = {}
93
+ # Method to create Renderer from Precompiled object, for example when you want to reuse precompiled object from disk
94
+ #
95
+ # @param [Precompiled] precompiled
96
+ #
97
+ # @return [Renderer] preconfigured instance of this class
98
+ #
99
+ def self.from_precompiled(precompiled)
100
+ inst = new
101
+ inst.precompiled = precompiled
102
+ inst
51
103
  end
52
104
 
53
-
54
105
  # ----------------------------------------------------------------------------- #
55
106
  # DSL methods
56
107
 
@@ -60,76 +111,145 @@ module Bade
60
111
  # @return [self]
61
112
  #
62
113
  def with_locals(locals = {})
114
+ self.render_binding = nil
115
+
63
116
  self.locals = locals
64
117
  self
65
118
  end
66
119
 
120
+ def with_binding(binding)
121
+ self.lambda_binding = binding
122
+ self
123
+ end
124
+
67
125
 
68
126
  # ----------------------------------------------------------------------------- #
69
127
  # Getters
70
128
 
71
- # @return [Bade::Node]
129
+ # @return [Bade::AST::Node]
72
130
  #
73
131
  def root_document
74
- @parsed ||= _parsed_document(source_text, file_path)
132
+ @root_document ||= _parsed_document(source_text, file_path)
133
+ end
134
+
135
+ # @return [Precompiled]
136
+ #
137
+ attr_writer :precompiled
138
+
139
+ # @return [Precompiled]
140
+ #
141
+ def precompiled
142
+ @precompiled ||= Precompiled.new(Generator.document_to_lambda_string(root_document), file_path)
75
143
  end
76
144
 
77
145
  # @return [String]
78
146
  #
79
- def lambda_string(new_line: '\n', indent: ' ')
80
- RubyGenerator.document_to_lambda_string(root_document, new_line: new_line, indent: indent)
147
+ def lambda_string
148
+ precompiled.code_string
81
149
  end
82
150
 
151
+ # @return [RenderBinding]
152
+ #
153
+ def render_binding
154
+ @render_binding ||= Runtime::RenderBinding.new(locals || {})
155
+ end
156
+
157
+ # @return [Proc]
158
+ #
159
+ def lambda_instance
160
+ if lambda_binding
161
+ lambda_binding.eval(lambda_string, file_path || TEMPLATE_FILE_NAME)
162
+ else
163
+ render_binding.instance_eval(lambda_string, file_path || TEMPLATE_FILE_NAME)
164
+ end
165
+ end
83
166
 
84
167
  # ----------------------------------------------------------------------------- #
85
168
  # Render
86
169
 
87
- # @return [String]
170
+ # @param [Binding] binding custom binding for evaluating the template, but it is not recommended to use, use :locals and #with_locals instead
171
+ # @param [String] new_line newline string, default is \n
172
+ # @param [String] indent indent string, default is two spaces
88
173
  #
89
- def render(binding: nil, new_line: '\n', indent: ' ')
90
- lambda_str = lambda_string(new_line: new_line, indent: indent)
91
- scope = binding || Runtime::RenderBinding.new(locals || {}).get_binding
174
+ # @return [String] rendered content of template
175
+ #
176
+ def render(binding: nil, new_line: nil, indent: nil)
177
+ self.lambda_binding = binding unless binding.nil? # backward compatibility
178
+
179
+ run_vars = {
180
+ Generator::NEW_LINE_NAME.to_sym => new_line,
181
+ Generator::BASE_INDENT_NAME.to_sym => indent,
182
+ }
183
+ run_vars.reject! { |_key, value| value.nil? } # remove nil values
92
184
 
93
- lambda_instance = eval(lambda_str, scope, file_path || '(__template__)')
94
- lambda_instance.call
185
+ lambda_instance.call(**run_vars)
95
186
  end
96
187
 
97
188
 
98
189
 
99
190
  private
100
191
 
101
- # @param file_path [String]
192
+ # @param [String] content source code of the template
193
+ # @param [String] file_path reference path to template file
102
194
  #
103
- # @return [Bade::Document]
195
+ # @return [Bade::AST::Document]
104
196
  #
105
197
  def _parsed_document(content, file_path)
106
198
  content = if file_path.nil? && content.nil?
107
- raise LoadError, "Don't know what to do with nil values for both content and path"
199
+ raise LoadError.new(nil, file_path, "Don't know what to do with nil values for both content and path")
108
200
  elsif !file_path.nil? && content.nil?
109
201
  File.read(file_path)
110
202
  else
111
203
  content
112
204
  end
113
205
 
114
- parsed_document = @parsed_documents[file_path]
206
+ parsed_document = parsed_documents[file_path]
115
207
  return parsed_document unless parsed_document.nil?
116
208
 
117
209
  parser = Parser.new(file_path: file_path)
118
-
119
210
  document = parser.parse(content)
120
211
 
121
212
  parser.dependency_paths.each do |path|
122
- sub_path = File.expand_path(path, File.dirname(file_path))
123
- new_path = if File.exists?(sub_path)
124
- sub_path
125
- elsif File.exists?("#{sub_path}.bade")
126
- "#{sub_path}.bade"
127
- end
213
+ new_path = _find_file!(path, file_path)
214
+ next if new_path.nil?
128
215
 
129
216
  document.sub_documents << _parsed_document(nil, new_path)
130
217
  end
131
218
 
132
219
  document
133
220
  end
221
+
222
+ # Tries to find file with name, if no file could be found or there are multiple files matching the name error is raised
223
+ #
224
+ # @param [String] name name of the file that should be found
225
+ # @param [String] reference_path path to file from which is loading/finding
226
+ #
227
+ # @return [String, nil] returns nil when this file should be skipped otherwise absolute path to file
228
+ #
229
+ def _find_file!(name, reference_path)
230
+ sub_path = File.expand_path(name, File.dirname(reference_path))
231
+
232
+ if File.exists?(sub_path)
233
+ return if sub_path.end_with?('.rb') # handled in Generator
234
+ sub_path
235
+ else
236
+ bade_path = "#{sub_path}.bade"
237
+ rb_path = "#{sub_path}.rb"
238
+
239
+ bade_exist = File.exists?(bade_path)
240
+ rb_exist = File.exists?(rb_path)
241
+ relative = Pathname.new(reference_path).relative_path_from(Pathname.new(File.dirname(self.file_path))).to_s
242
+
243
+ if bade_exist && rb_exist
244
+ raise LoadError.new(name, reference_path, "Found both .bade and .rb files for `#{name}` in file #{relative}, change the import path so it references uniq file.")
245
+ elsif bade_exist
246
+ return bade_path
247
+ elsif rb_exist
248
+ return # handled in Generator
249
+ else
250
+ raise LoadError.new(name, reference_path, "Can't find file matching name `#{name}` referenced from file #{relative}")
251
+ end
252
+ end
253
+ end
134
254
  end
135
255
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Array
4
+ # Returns index of last matching item when iterating from back to start of +self+.
5
+ #
6
+ # Returns nil when the first item does not match (when iterating from back).
7
+ #
8
+ # @return [Fixnum]
9
+ #
10
+ def rindex_last_matching(&block)
11
+ return nil if empty?
12
+
13
+ index = nil
14
+
15
+ current_index = count - 1
16
+ reverse_each do |item|
17
+ if block.call(item)
18
+ index = current_index
19
+ current_index -= 1
20
+ else
21
+ break
22
+ end
23
+ end
24
+
25
+ index
26
+ end
27
+
28
+ # Returns count of items that matches, iteration starts at the end and stops on first not matching item.
29
+ #
30
+ # @return [Fixnum] count of items
31
+ #
32
+ def rcount_matching(&block)
33
+ count = 0
34
+
35
+ reverse_each do |item|
36
+ if block.call(item)
37
+ count += 1
38
+ else
39
+ break
40
+ end
41
+ end
42
+
43
+ count
44
+ end
45
+ end
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class String
4
+ SPACE_CHAR = ' '
5
+ TAB_CHAR = "\t"
2
6
 
3
7
  # Creates new string surrounded by single quotes
4
8
  #
@@ -9,30 +13,37 @@ class String
9
13
  end
10
14
 
11
15
 
12
- # Remove indent
13
- #
14
- # @param [Int] indent
15
- # @param [Int] tabsize
16
- #
17
- def remove_indent(indent, tabsize)
18
- self.dup.remove_indent!(indent, tabsize)
16
+ def blank?
17
+ strip.length == 0
19
18
  end
20
19
 
21
20
 
22
- # Remove indent
23
- #
24
- # @param [Int] indent
25
- # @param [Int] tabsize
26
- #
27
- def remove_indent!(indent, tabsize)
21
+ def remove_last(count = 1)
22
+ slice(0, length - count)
23
+ end
24
+
25
+ def remove_last!(count = 1)
26
+ slice!(length - count, count)
27
+ end
28
+
29
+
30
+ def remove_first(count = 1)
31
+ slice(count, length - count)
32
+ end
33
+
34
+ def remove_first!(count = 1)
35
+ slice!(0, count)
36
+ end
37
+
38
+ def __chars_count_for_indent(indent, tabsize)
28
39
  count = 0
29
40
  self.each_char do |char|
41
+ break if indent <= 0
30
42
 
31
- if indent <= 0
32
- break
33
- elsif char == ' '
43
+ case char
44
+ when SPACE_CHAR
34
45
  indent -= 1
35
- elsif char == "\t"
46
+ when TAB_CHAR
36
47
  if indent - tabsize < 0
37
48
  raise StandardError, 'malformed tabs'
38
49
  end
@@ -45,7 +56,26 @@ class String
45
56
  count += 1
46
57
  end
47
58
 
48
- self[0 ... self.length] = self[count ... self.length]
59
+ count
60
+ end
61
+
62
+ # Remove indent
63
+ #
64
+ # @param [Int] indent
65
+ # @param [Int] tabsize
66
+ #
67
+ def remove_indent(indent, tabsize)
68
+ remove_first(__chars_count_for_indent(indent, tabsize))
69
+ end
70
+
71
+
72
+ # Remove indent
73
+ #
74
+ # @param [Int] indent
75
+ # @param [Int] tabsize
76
+ #
77
+ def remove_indent!(indent, tabsize)
78
+ remove_first!(__chars_count_for_indent(indent, tabsize))
49
79
  end
50
80
 
51
81
 
@@ -59,9 +89,9 @@ class String
59
89
  count = 0
60
90
 
61
91
  self.each_char do |char|
62
- if char == ' '
92
+ if char == SPACE_CHAR
63
93
  count += 1
64
- elsif char == "\t"
94
+ elsif char == TAB_CHAR
65
95
  count += tabsize
66
96
  else
67
97
  break