flavour_saver 4.0.2 → 5.0.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +14 -0
  3. data/.standard.yml +1 -0
  4. data/CHANGELOG.md +57 -0
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +48 -5
  7. data/README.md +153 -271
  8. data/flavour_saver.gemspec +17 -19
  9. data/lib/flavour_saver/error.rb +5 -0
  10. data/lib/flavour_saver/helpers.rb +19 -18
  11. data/lib/flavour_saver/lexer.rb +123 -50
  12. data/lib/flavour_saver/nodes.rb +107 -19
  13. data/lib/flavour_saver/parser.rb +339 -161
  14. data/lib/flavour_saver/partial.rb +4 -6
  15. data/lib/flavour_saver/rails_partial.rb +10 -4
  16. data/lib/flavour_saver/runtime.rb +76 -81
  17. data/lib/flavour_saver/template.rb +3 -5
  18. data/lib/flavour_saver/version.rb +1 -1
  19. data/lib/flavour_saver.rb +17 -17
  20. data/spec/acceptance/backtrack_spec.rb +9 -9
  21. data/spec/acceptance/comment_spec.rb +7 -7
  22. data/spec/acceptance/custom_block_helper_spec.rb +12 -12
  23. data/spec/acceptance/custom_helper_spec.rb +8 -8
  24. data/spec/acceptance/ensure_no_rce_spec.rb +64 -53
  25. data/spec/acceptance/handlebars_qunit_spec.rb +295 -297
  26. data/spec/acceptance/if_else_spec.rb +6 -6
  27. data/spec/acceptance/multi_level_with_spec.rb +10 -11
  28. data/spec/acceptance/nesting_depth_spec.rb +34 -0
  29. data/spec/acceptance/one_character_identifier_spec.rb +8 -8
  30. data/spec/acceptance/raw_block_spec.rb +7 -7
  31. data/spec/acceptance/sections_spec.rb +17 -14
  32. data/spec/acceptance/segment_literals_spec.rb +5 -5
  33. data/spec/acceptance/simple_expression_spec.rb +9 -9
  34. data/spec/acceptance/subexpression_spec.rb +18 -17
  35. data/spec/acceptance/unless_spec.rb +19 -6
  36. data/spec/lib/flavour_saver/lexer_spec.rb +103 -89
  37. data/spec/lib/flavour_saver/nodes_spec.rb +85 -0
  38. data/spec/lib/flavour_saver/parser_spec.rb +155 -95
  39. data/spec/lib/flavour_saver/runtime_spec.rb +94 -96
  40. data/spec/lib/flavour_saver/template_spec.rb +1 -2
  41. metadata +19 -48
@@ -1,4 +1,4 @@
1
- require 'delegate'
1
+ require "delegate"
2
2
 
3
3
  module FlavourSaver
4
4
  module Helpers
@@ -11,19 +11,19 @@ module FlavourSaver
11
11
  r = []
12
12
  count = 0
13
13
  collection.each do |element|
14
- locals ={
15
- 'index' => count,
16
- 'last' => count == collection.size - 1,
17
- 'first' => count == 0
14
+ locals = {
15
+ "index" => count,
16
+ "last" => count == collection.size - 1,
17
+ "first" => count == 0
18
18
  }
19
19
 
20
- locals['key'], element = element if collection.is_a?(Hash)
20
+ locals["key"], element = element if collection.is_a?(Hash)
21
21
 
22
22
  r << yield.contents(element, locals)
23
23
  count += 1
24
24
  end
25
25
  yield.rendered!
26
- r.join ''
26
+ r.join ""
27
27
  end
28
28
 
29
29
  def if(value)
@@ -44,7 +44,7 @@ module FlavourSaver
44
44
 
45
45
  def log(message)
46
46
  FS.logger.debug("FlavourSaver: #{message}")
47
- ''
47
+ ""
48
48
  end
49
49
 
50
50
  private
@@ -59,11 +59,10 @@ module FlavourSaver
59
59
  end
60
60
 
61
61
  class Decorator < Defaults
62
-
63
62
  def initialize(locals, source)
64
63
  @source = source
65
64
  mixin = Module.new do
66
- locals.each do |name,impl|
65
+ locals.each do |name, impl|
67
66
  define_method name, &impl
68
67
  end
69
68
  end
@@ -88,7 +87,9 @@ module FlavourSaver
88
87
  super || @source.respond_to?(name)
89
88
  end
90
89
 
91
- def method_missing(name,*args,&b)
90
+ # Defining respond_to_missing? would change what #method returns, which
91
+ # Runtime#forbidden_method? relies on. See the SECURITY note below.
92
+ def method_missing(name, *args, &b) # standard:disable Style/MissingRespondToMissing
92
93
  # I would rather have it raise a NameError, but Moustache
93
94
  # compatibility requires that missing helpers return
94
95
  # nothing. A good place for bugs to hide.
@@ -116,7 +117,7 @@ module FlavourSaver
116
117
  @registered_helpers ||= {}
117
118
  end
118
119
 
119
- def register_helper(method,&b)
120
+ def register_helper(method, &b)
120
121
  if !method.is_a?(Symbol) && method.respond_to?(:name)
121
122
  registered_helpers[method.name.to_sym] = method
122
123
  elsif b
@@ -134,13 +135,13 @@ module FlavourSaver
134
135
  @registered_helpers = {}
135
136
  end
136
137
 
137
- def decorate_with(context, helper_names=[], locals={})
138
+ def decorate_with(context, helper_names = [], locals = {})
138
139
  helpers = if helper_names.any?
139
- helper_symbols = helper_names.map(&:to_sym)
140
- registered_helpers.select { |k,v| helper_symbols.member? k }
141
- else
142
- registered_helpers
143
- end
140
+ helper_symbols = helper_names.map(&:to_sym)
141
+ registered_helpers.select { |k, v| helper_symbols.member? k }
142
+ else
143
+ registered_helpers
144
+ end
144
145
  helpers = helpers.merge(locals)
145
146
  Decorator.new(helpers, context)
146
147
  end
@@ -1,124 +1,168 @@
1
- require 'rltk'
1
+ require "strscan"
2
+ require "flavour_saver/error"
2
3
 
3
4
  module FlavourSaver
4
- class Lexer < RLTK::Lexer
5
+ class Lexer
6
+ Token = Struct.new(:type, :value)
5
7
 
6
- # seems to have problem with hash symbol in regex
7
- rule /\{\{\{\{raw\}\}\}\}/, :default do
8
+ class LexingError < Error; end
9
+
10
+ # How much of the unmatched input a LexingError message quotes.
11
+ ERROR_EXCERPT_LENGTH = 50
12
+
13
+ Rule = Struct.new(:pattern, :action)
14
+
15
+ # Rules are grouped by lexer state. At each position every rule for the
16
+ # current state is tried and the longest match wins; on a tie the rule
17
+ # defined first wins. An action returns nil (no token), a token type, or
18
+ # a [type, value] pair.
19
+ def self.rules
20
+ @rules ||= Hash.new { |h, k| h[k] = [] }
21
+ end
22
+
23
+ def self.rule(pattern, state = :default, &action)
24
+ rules[state] << Rule.new(pattern, action || proc {})
25
+ end
26
+
27
+ def self.lex(template)
28
+ new.lex(template)
29
+ end
30
+
31
+ def lex(template)
32
+ @states = [:default]
33
+ tokens = []
34
+ scanner = StringScanner.new(template)
35
+
36
+ until scanner.eos?
37
+ match, rule = longest_match(scanner)
38
+ unless rule
39
+ raise LexingError, "Unable to match string with any of the given rules: #{excerpt(scanner.rest)}"
40
+ end
41
+
42
+ scanner.pos += match.bytesize
43
+ type, value = instance_exec(match, &rule.action)
44
+ tokens << Token.new(type, value) if type
45
+ end
46
+
47
+ tokens << Token.new(:EOS)
48
+ end
49
+
50
+ # seems to have problem with hash symbol in regex
51
+ rule(/\{\{\{\{raw\}\}\}\}/, :default) do
8
52
  push_state :raw
9
53
  :RAWSTART
10
54
  end
11
55
 
12
- rule /.*?(?=\{\{\{\{\/raw\}\}\}\})/m, :raw do |str|
13
- [ :RAWSTRING, str ]
56
+ rule(/.*?(?=\{\{\{\{\/raw\}\}\}\})/m, :raw) do |str|
57
+ [:RAWSTRING, str]
14
58
  end
15
59
 
16
- rule /\{\{\{\{\/raw\}\}\}\}/, :raw do
60
+ rule(/\{\{\{\{\/raw\}\}\}\}/, :raw) do
17
61
  pop_state
18
62
  :RAWEND
19
63
  end
20
64
 
21
- rule /{{{/, :default do
65
+ rule(/{{{/, :default) do
22
66
  push_state :expression
23
67
  :TEXPRST
24
68
  end
25
69
 
26
- rule /{{/, :default do
70
+ rule(/{{/, :default) do
27
71
  push_state :expression
28
72
  :EXPRST
29
73
  end
30
74
 
31
- rule /#/, :expression do
75
+ rule(/#/, :expression) do
32
76
  :HASH
33
77
  end
34
78
 
35
- rule /\//, :expression do
79
+ rule(/\//, :expression) do
36
80
  :FWSL
37
81
  end
38
82
 
39
- rule /&/, :expression do
83
+ rule(/&/, :expression) do
40
84
  :AMP
41
85
  end
42
86
 
43
- rule /\^/, :expression do
87
+ rule(/\^/, :expression) do
44
88
  :HAT
45
89
  end
46
90
 
47
- rule /@/, :expression do
91
+ rule(/@/, :expression) do
48
92
  :AT
49
93
  end
50
94
 
51
- rule />/, :expression do
95
+ rule(/>/, :expression) do
52
96
  :GT
53
97
  end
54
98
 
55
- rule /([0-9]+(\.[0-9]+)?)/, :expression do |n|
56
- [ :NUMBER, n ]
99
+ rule(/([0-9]+(\.[0-9]+)?)/, :expression) do |n|
100
+ [:NUMBER, n]
57
101
  end
58
102
 
59
- rule /true/, :expression do |i|
60
- [ :BOOL, true ]
103
+ rule(/true/, :expression) do |i|
104
+ [:BOOL, true]
61
105
  end
62
106
 
63
- rule /false/, :expression do |i|
64
- [ :BOOL, false ]
107
+ rule(/false/, :expression) do |i|
108
+ [:BOOL, false]
65
109
  end
66
110
 
67
- rule /\!/, :expression do
111
+ rule(/!/, :expression) do
68
112
  push_state :comment
69
113
  :BANG
70
114
  end
71
115
 
72
- rule /([^}}]*)/, :comment do |comment|
116
+ rule(/([^}]*)/, :comment) do |comment|
73
117
  pop_state
74
- [ :COMMENT, comment ]
118
+ [:COMMENT, comment]
75
119
  end
76
120
 
77
- rule /else/, :expression do
121
+ rule(/else/, :expression) do
78
122
  :ELSE
79
123
  end
80
124
 
81
- rule /([A-Za-z_]\w*)/, :expression do |name|
82
- [ :IDENT, name ]
125
+ rule(/([A-Za-z_]\w*)/, :expression) do |name|
126
+ [:IDENT, name]
83
127
  end
84
128
 
85
- rule /\./, :expression do
129
+ rule(/\./, :expression) do
86
130
  :DOT
87
131
  end
88
132
 
89
- rule /\(/, :expression do
133
+ rule(/\(/, :expression) do
90
134
  :OPAR
91
135
  end
92
136
 
93
- rule /\)/, :expression do
137
+ rule(/\)/, :expression) do
94
138
  :CPAR
95
139
  end
96
140
 
97
- rule /\=/, :expression do
141
+ rule(/=/, :expression) do
98
142
  :EQ
99
143
  end
100
144
 
101
- rule /"/, :expression do
145
+ rule(/"/, :expression) do
102
146
  push_state :string
103
147
  end
104
148
 
105
- rule /(\\"|[^"])*/, :string do |str|
106
- [ :STRING, str ]
149
+ rule(/(\\"|[^"])*/, :string) do |str|
150
+ [:STRING, str]
107
151
  end
108
152
 
109
- rule /"/, :string do
153
+ rule(/"/, :string) do
110
154
  pop_state
111
155
  end
112
156
 
113
- rule /'/, :expression do
157
+ rule(/'/, :expression) do
114
158
  push_state :s_string
115
159
  end
116
160
 
117
- rule /(\\'|[^'])*/, :s_string do |str|
118
- [ :S_STRING, str ]
161
+ rule(/(\\'|[^'])*/, :s_string) do |str|
162
+ [:S_STRING, str]
119
163
  end
120
164
 
121
- rule /'/, :s_string do
165
+ rule(/'/, :s_string) do
122
166
  pop_state
123
167
  end
124
168
 
@@ -130,38 +174,67 @@ module FlavourSaver
130
174
  # Identifiers may be any unicode character except for the following:
131
175
  # Whitespace ! " # % & ' ( ) * + , . / ; < = > @ [ \ ] ^ ` { | } ~
132
176
  #
133
- rule /([^\s!-#%-,.\/;->@\[-^`{-~]+)/, :expression do |str|
134
- [ :LITERAL, str ]
177
+ rule(/([^\s!-#%-,.\/;->@\[-^`{-~]+)/, :expression) do |str|
178
+ [:LITERAL, str]
135
179
  end
136
180
 
137
- rule /\[/, :expression do
181
+ rule(/\[/, :expression) do
138
182
  push_state :segment_literal
139
183
  end
140
184
 
141
- rule /([^\]]+)/, :segment_literal do |l|
142
- [ :LITERAL, l ]
185
+ rule(/([^\]]+)/, :segment_literal) do |l|
186
+ [:LITERAL, l]
143
187
  end
144
188
 
145
- rule /]/, :segment_literal do
189
+ rule(/]/, :segment_literal) do
146
190
  pop_state
147
191
  end
148
192
 
149
- rule /\s+/, :expression do
193
+ rule(/\s+/, :expression) do
150
194
  :WHITE
151
195
  end
152
196
 
153
- rule /}}}/, :expression do
197
+ rule(/}}}/, :expression) do
154
198
  pop_state
155
199
  :TEXPRE
156
200
  end
157
201
 
158
- rule /}}/, :expression do
202
+ rule(/}}/, :expression) do
159
203
  pop_state
160
204
  :EXPRE
161
205
  end
162
206
 
163
- rule /.*?(?={{|\z)/m, :default do |output|
164
- [ :OUT, output ]
207
+ rule(/.*?(?={{|\z)/m, :default) do |output|
208
+ [:OUT, output]
209
+ end
210
+
211
+ private
212
+
213
+ def longest_match(scanner)
214
+ best = nil
215
+ self.class.rules[state].each do |rule|
216
+ text = scanner.check(rule.pattern)
217
+ best = [text, rule] if text && (best.nil? || best.first.length < text.length)
218
+ end
219
+ best
220
+ end
221
+
222
+ def excerpt(rest)
223
+ (rest.length > ERROR_EXCERPT_LENGTH) ? "#{rest[0, ERROR_EXCERPT_LENGTH]}..." : rest
224
+ end
225
+
226
+ def state
227
+ @states.last
228
+ end
229
+
230
+ def push_state(state)
231
+ @states.push(state)
232
+ nil
233
+ end
234
+
235
+ def pop_state
236
+ @states.pop
237
+ nil
165
238
  end
166
239
  end
167
240
  end
@@ -1,12 +1,99 @@
1
- require 'rltk'
2
- require 'rltk/ast'
3
- require 'flavour_saver/nodes'
4
-
5
1
  module FlavourSaver
6
- class Node < RLTK::ASTNode
2
+ class Node
3
+ class TypeMismatch < StandardError; end
4
+
5
+ # Fields are declared as either values (plain data) or children (nodes,
6
+ # or arrays of nodes, which get their #parent set). The constructor takes
7
+ # every value, then every child, positionally, inherited ones first.
8
+ # A field's type is a class (nil is also allowed) or a class in an array,
9
+ # such as [CallNode].
10
+ @value_names = []
11
+ @child_names = []
12
+
13
+ class << self
14
+ attr_reader :value_names, :child_names
15
+
16
+ # Copy the parent's fields now, not on first read, so they can't depend on read order.
17
+ def inherited(subclass)
18
+ super
19
+ subclass.instance_variable_set(:@value_names, value_names.dup)
20
+ subclass.instance_variable_set(:@child_names, child_names.dup)
21
+ end
22
+
23
+ def value(name, type)
24
+ if field_class(name, type) <= Node
25
+ raise ArgumentError, "#{self}##{name} is a value, so its type can't be a Node"
26
+ end
27
+ value_names << name
28
+ attr_reader name
29
+ ivar = :"@#{name}"
30
+ define_method(:"#{name}=") do |value|
31
+ check_field_type(name, type, value)
32
+ instance_variable_set(ivar, value)
33
+ end
34
+ end
35
+
36
+ def child(name, type)
37
+ unless field_class(name, type) <= Node
38
+ raise ArgumentError, "#{self}##{name} is a child, so its type must be a Node"
39
+ end
40
+ child_names << name
41
+ attr_reader name
42
+ ivar = :"@#{name}"
43
+ define_method(:"#{name}=") do |node|
44
+ check_field_type(name, type, node)
45
+ Array(node).each { |n| n.parent = self }
46
+ instance_variable_set(ivar, node)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def field_class(name, type)
53
+ klass = (type.is_a?(Array) && type.size == 1) ? type.first : type
54
+ return klass if klass.is_a?(Class)
55
+ raise ArgumentError, "#{self}##{name} must have a class, or a class in an array, as its type"
56
+ end
57
+ end
58
+
59
+ attr_accessor :parent
60
+
61
+ def initialize(*fields)
62
+ names = self.class.value_names + self.class.child_names
63
+ raise ArgumentError, "#{self.class} takes at most #{names.size} fields" if fields.size > names.size
64
+ names.each_with_index { |name, i| public_send(:"#{name}=", fields[i]) }
65
+ end
66
+
67
+ def values
68
+ self.class.value_names.map { |name| public_send(name) }
69
+ end
70
+
71
+ def children
72
+ self.class.child_names.map { |name| public_send(name) }
73
+ end
74
+
75
+ def ==(other)
76
+ other.class == self.class && other.values == values && other.children == children
77
+ end
78
+
7
79
  def inspect
8
80
  to_s.inspect
9
81
  end
82
+
83
+ private
84
+
85
+ def check_field_type(name, type, value)
86
+ valid =
87
+ if type.is_a?(Array)
88
+ value.is_a?(Array) && value.all? { |item| item.is_a?(type.first) }
89
+ else
90
+ value.nil? || value.is_a?(type)
91
+ end
92
+ return if valid
93
+
94
+ actual = (value.is_a?(Array) && value.any?) ? "an Array of #{value.map(&:class).uniq.join(", ")}" : value.class
95
+ raise TypeMismatch, "#{self.class}##{name} must be #{type.inspect}, not #{actual}"
96
+ end
10
97
  end
11
98
 
12
99
  class TemplateItemNode < Node; end
@@ -15,7 +102,7 @@ module FlavourSaver
15
102
  child :items, [TemplateItemNode]
16
103
 
17
104
  def to_s
18
- items.map(&:to_s).join ''
105
+ items.join ""
19
106
  end
20
107
  end
21
108
 
@@ -60,14 +147,14 @@ module FlavourSaver
60
147
  value :name, String
61
148
  value :arguments, Array
62
149
 
63
- def arguments_to_str(str='')
150
+ def arguments_to_str(str = "")
64
151
  str = str.dup # RLTK magic?
65
152
  arguments.each do |arg|
66
- str << ' '
153
+ str << " "
67
154
  if arg.respond_to? :join
68
- str << arg.join('.')
155
+ str << arg.join(".")
69
156
  elsif arg.respond_to? :keys
70
- arg.each do |k,v|
157
+ arg.each do |k, v|
71
158
  str << "#{k}: #{v.inspect}"
72
159
  end
73
160
  else
@@ -98,34 +185,35 @@ module FlavourSaver
98
185
  value :depth, Integer
99
186
 
100
187
  def to_callnode
101
- CallNode.new(name,arguments)
188
+ CallNode.new(name, arguments)
102
189
  end
190
+
103
191
  def to_s
104
- "#{'../' * depth}#{super}"
192
+ "#{"../" * depth}#{super}"
105
193
  end
106
194
  end
107
195
 
108
196
  class ExpressionNode < TemplateItemNode
109
197
  child :method, [CallNode]
110
198
  def to_s
111
- "{{#{method.map(&:to_s).join '.'}}}"
199
+ "{{#{method.join "."}}}"
112
200
  end
113
201
  end
114
202
 
115
203
  class BlockExpressionNode < ExpressionNode
116
204
  child :contents, TemplateNode
117
- child :closer, CallNode
205
+ child :closer, CallNode
118
206
 
119
207
  def name
120
208
  method.first.name
121
209
  end
122
210
 
123
211
  def to_s
124
- "{{##{method.map(&:to_s).join ''}}}#{contents.to_s}{{/#{closer.name}}}"
212
+ "{{##{method.join ""}}}#{contents}{{/#{closer.name}}}"
125
213
  end
126
214
 
127
215
  def inspect
128
- r = "{{##{method.map(&:to_s).join ''}}}\n"
216
+ r = "{{##{method.join ""}}}\n"
129
217
  r << " "
130
218
  r << contents.inspect.split("\n").join("\n ")
131
219
  r
@@ -136,11 +224,11 @@ module FlavourSaver
136
224
  child :alternate, TemplateNode
137
225
 
138
226
  def to_s
139
- "{{##{method.map(&:to_s).join ''}}}#{contents.to_s}{{else}}#{alternate.to_s}{{/#{closer.name}}}"
227
+ "{{##{method.join ""}}}#{contents}{{else}}#{alternate}{{/#{closer.name}}}"
140
228
  end
141
229
 
142
230
  def inspect
143
- r = "{{##{method.map(&:to_s).join ''}}}\n"
231
+ r = "{{##{method.join ""}}}\n"
144
232
  r << contents.inspect.split("\n").join("\n ")
145
233
  r << "\n {{else}}\n"
146
234
  r << alternate.inspect.split("\n").join("\n ")
@@ -150,7 +238,7 @@ module FlavourSaver
150
238
 
151
239
  class SafeExpressionNode < ExpressionNode
152
240
  def to_s
153
- "{{{#{method.map(&:to_s).join '.'}}}}"
241
+ "{{{#{method.join "."}}}}"
154
242
  end
155
243
  end
156
244