bade 0.1.4 → 0.2.0

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