luoma 0.1.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 (152) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +2 -0
  3. data/.rdoc_options +16 -0
  4. data/CHANGELOG.md +5 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +68 -0
  7. data/Rakefile +69 -0
  8. data/Steepfile +41 -0
  9. data/certs/jgrp.pem +27 -0
  10. data/docs/configuration.md +180 -0
  11. data/docs/custom_filters.md +3 -0
  12. data/docs/custom_tags.md +3 -0
  13. data/docs/expressions.md +3 -0
  14. data/docs/extension_types.md +41 -0
  15. data/docs/filter_reference.md +1702 -0
  16. data/docs/index.md +73 -0
  17. data/docs/luoma_for_template_authors.md +3 -0
  18. data/docs/markdown.md +111 -0
  19. data/docs/predicate_reference.md +3 -0
  20. data/docs/static_analysis.md +3 -0
  21. data/docs/tag_reference.md +352 -0
  22. data/docs/template_loaders.md +177 -0
  23. data/docs/undefined_variables.md +3 -0
  24. data/docs-requirements.txt +11 -0
  25. data/docs_/cycle.md +17 -0
  26. data/docs_/font_rendering_example.md +157 -0
  27. data/docs_/header_block_example.md +51 -0
  28. data/docs_/increment_and_decrement.md +49 -0
  29. data/docs_/logo_style_example.md +40 -0
  30. data/docs_/migration.md +11 -0
  31. data/docs_/notes.md +19 -0
  32. data/lib/luoma/cache.rb +80 -0
  33. data/lib/luoma/chain_hash.rb +54 -0
  34. data/lib/luoma/context.rb +227 -0
  35. data/lib/luoma/drop.rb +84 -0
  36. data/lib/luoma/drops/block.rb +33 -0
  37. data/lib/luoma/drops/expression.rb +29 -0
  38. data/lib/luoma/drops/html_safe.rb +36 -0
  39. data/lib/luoma/drops/range.rb +69 -0
  40. data/lib/luoma/drops/undefined.rb +119 -0
  41. data/lib/luoma/environment.rb +479 -0
  42. data/lib/luoma/errors.rb +79 -0
  43. data/lib/luoma/escape.rb +35 -0
  44. data/lib/luoma/expression.rb +1271 -0
  45. data/lib/luoma/filter.rb +97 -0
  46. data/lib/luoma/filters/array.rb +372 -0
  47. data/lib/luoma/filters/date.rb +19 -0
  48. data/lib/luoma/filters/default.rb +16 -0
  49. data/lib/luoma/filters/json.rb +14 -0
  50. data/lib/luoma/filters/math.rb +84 -0
  51. data/lib/luoma/filters/size.rb +13 -0
  52. data/lib/luoma/filters/slice.rb +52 -0
  53. data/lib/luoma/filters/sort.rb +113 -0
  54. data/lib/luoma/filters/string.rb +186 -0
  55. data/lib/luoma/fnv.rb +15 -0
  56. data/lib/luoma/lexer.rb +106 -0
  57. data/lib/luoma/lexer_legacy.rb +396 -0
  58. data/lib/luoma/lexer_unified.rb +279 -0
  59. data/lib/luoma/loader.rb +50 -0
  60. data/lib/luoma/loaders/choice_loader.rb +20 -0
  61. data/lib/luoma/loaders/file_system_loader.rb +75 -0
  62. data/lib/luoma/loaders/mixins.rb +52 -0
  63. data/lib/luoma/markup.rb +109 -0
  64. data/lib/luoma/parser.rb +252 -0
  65. data/lib/luoma/parser_unified.rb +979 -0
  66. data/lib/luoma/predicates/blank.rb +19 -0
  67. data/lib/luoma/predicates/defined.rb +10 -0
  68. data/lib/luoma/predicates/empty.rb +15 -0
  69. data/lib/luoma/predicates/type.rb +35 -0
  70. data/lib/luoma/static_analysis.rb +427 -0
  71. data/lib/luoma/tags/assign.rb +63 -0
  72. data/lib/luoma/tags/break.rb +23 -0
  73. data/lib/luoma/tags/capture.rb +43 -0
  74. data/lib/luoma/tags/case.rb +137 -0
  75. data/lib/luoma/tags/comment.rb +17 -0
  76. data/lib/luoma/tags/continue.rb +23 -0
  77. data/lib/luoma/tags/define.rb +42 -0
  78. data/lib/luoma/tags/else.rb +30 -0
  79. data/lib/luoma/tags/for.rb +108 -0
  80. data/lib/luoma/tags/if.rb +109 -0
  81. data/lib/luoma/tags/import.rb +91 -0
  82. data/lib/luoma/tags/include.rb +77 -0
  83. data/lib/luoma/tags/output.rb +20 -0
  84. data/lib/luoma/tags/raw.rb +26 -0
  85. data/lib/luoma/tags/render.rb +92 -0
  86. data/lib/luoma/tags/with.rb +55 -0
  87. data/lib/luoma/template.rb +178 -0
  88. data/lib/luoma/token.rb +84 -0
  89. data/lib/luoma/version.rb +5 -0
  90. data/lib/luoma.rb +75 -0
  91. data/sig/luoma/cache.rbs +44 -0
  92. data/sig/luoma/chain_hash.rbs +26 -0
  93. data/sig/luoma/context.rbs +115 -0
  94. data/sig/luoma/drop.rbs +44 -0
  95. data/sig/luoma/drops/block.rbs +10 -0
  96. data/sig/luoma/drops/expression.rbs +10 -0
  97. data/sig/luoma/drops/html_safe.rbs +14 -0
  98. data/sig/luoma/drops/range.rbs +15 -0
  99. data/sig/luoma/drops/undefined.rbs +27 -0
  100. data/sig/luoma/environment.rbs +212 -0
  101. data/sig/luoma/errors.rbs +67 -0
  102. data/sig/luoma/escape.rbs +15 -0
  103. data/sig/luoma/expression.rbs +514 -0
  104. data/sig/luoma/filter.rbs +66 -0
  105. data/sig/luoma/filters/array.rbs +74 -0
  106. data/sig/luoma/filters/date.rbs +9 -0
  107. data/sig/luoma/filters/default.rbs +8 -0
  108. data/sig/luoma/filters/json.rbs +7 -0
  109. data/sig/luoma/filters/math.rbs +37 -0
  110. data/sig/luoma/filters/size.rbs +6 -0
  111. data/sig/luoma/filters/slice.rbs +10 -0
  112. data/sig/luoma/filters/sort.rbs +17 -0
  113. data/sig/luoma/filters/string.rbs +103 -0
  114. data/sig/luoma/fnv.rbs +3 -0
  115. data/sig/luoma/lexer.rbs +42 -0
  116. data/sig/luoma/lexer_legacy.rbs +81 -0
  117. data/sig/luoma/lexer_unified.rbs +49 -0
  118. data/sig/luoma/loader.rbs +40 -0
  119. data/sig/luoma/loaders/choice_loader.rbs +9 -0
  120. data/sig/luoma/loaders/file_system_loader.rbs +19 -0
  121. data/sig/luoma/loaders/mixins.rbs +16 -0
  122. data/sig/luoma/markup.rbs +68 -0
  123. data/sig/luoma/parser.rbs +105 -0
  124. data/sig/luoma/parser_unified.rbs +150 -0
  125. data/sig/luoma/predicates/blank.rbs +6 -0
  126. data/sig/luoma/predicates/defined.rbs +6 -0
  127. data/sig/luoma/predicates/empty.rbs +6 -0
  128. data/sig/luoma/predicates/type.rbs +21 -0
  129. data/sig/luoma/static_analysis.rbs +141 -0
  130. data/sig/luoma/tags/assign.rbs +12 -0
  131. data/sig/luoma/tags/break.rbs +11 -0
  132. data/sig/luoma/tags/capture.rbs +14 -0
  133. data/sig/luoma/tags/case.rbs +36 -0
  134. data/sig/luoma/tags/comment.rbs +11 -0
  135. data/sig/luoma/tags/continue.rbs +11 -0
  136. data/sig/luoma/tags/define.rbs +16 -0
  137. data/sig/luoma/tags/else.rbs +17 -0
  138. data/sig/luoma/tags/for.rbs +26 -0
  139. data/sig/luoma/tags/if.rbs +45 -0
  140. data/sig/luoma/tags/import.rbs +13 -0
  141. data/sig/luoma/tags/include.rbs +13 -0
  142. data/sig/luoma/tags/output.rbs +13 -0
  143. data/sig/luoma/tags/raw.rbs +11 -0
  144. data/sig/luoma/tags/render.rbs +13 -0
  145. data/sig/luoma/tags/with.rbs +15 -0
  146. data/sig/luoma/template.rbs +117 -0
  147. data/sig/luoma/token.rbs +81 -0
  148. data/sig/luoma.rbs +11 -0
  149. data/zensical.toml +363 -0
  150. data.tar.gz.sig +0 -0
  151. metadata +233 -0
  152. metadata.gz.sig +0 -0
@@ -0,0 +1,479 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Luoma
6
+ class Environment
7
+ attr_reader :persistent_registers
8
+
9
+ attr_accessor :auto_trim, :globals, :lexer, :loader, :parser, :strict,
10
+ :suppress_blank_control_flow_blocks, :undefined, :filters, :tags, :max_assign_score_cumulative,
11
+ :max_assign_score, :max_context_depth, :max_render_score_cumulative, :max_render_score,
12
+ :max_render_size, :predicates
13
+
14
+ RE_INTEGER = /\A-?\d+(?:[eE]\+?\d+)?\Z/
15
+ RE_DECIMAL = /((?:-?\d+\.\d+(?:[eE][+-]?\d+)?)|(-?\d+[eE]-\d+))/
16
+
17
+ def initialize(
18
+ auto_trim: nil,
19
+ globals: nil,
20
+ lexer: UnifiedLexer,
21
+ loader: nil,
22
+ max_assign_score_cumulative: nil,
23
+ max_assign_score: nil,
24
+ max_context_depth: 30,
25
+ max_render_score_cumulative: nil,
26
+ max_render_score: nil,
27
+ max_render_size: nil,
28
+ parser: UnifiedParser,
29
+ strict: false,
30
+ suppress_blank_control_flow_blocks: true,
31
+ undefined: UndefinedDrop
32
+ )
33
+ @auto_trim = auto_trim
34
+ @globals = globals
35
+ @lexer = lexer
36
+ @loader = loader || HashLoader.new({})
37
+ @max_assign_score = max_assign_score
38
+ @max_assign_score_cumulative = max_assign_score_cumulative
39
+ @max_context_depth = max_context_depth
40
+ @max_render_score = max_render_score
41
+ @max_render_score_cumulative = max_render_score_cumulative
42
+ @max_render_size = max_render_size
43
+ @parser = parser
44
+ @strict = strict
45
+ @suppress_blank_control_flow_blocks = suppress_blank_control_flow_blocks
46
+ @undefined = undefined
47
+
48
+ @tags = {} #: Hash[String, _Tag]
49
+ @filters = {} #: Hash[String, untyped]
50
+ @predicates = {} #: Hash[String, ^(untyped) -> bool]
51
+ setup_tags_filters_and_predicates
52
+
53
+ # Render context registers that persist when copying an instance of
54
+ # `RenderContext`.
55
+ @persistent_registers = Set[:extends_stack] #: Set[Symbol]
56
+ end
57
+
58
+ #: (String,
59
+ # ?globals: _Namespace?,
60
+ # ?name: String?,
61
+ # ?overlay: _Namespace?,
62
+ # ?up_to_date: Proc::_Callable?) -> Template
63
+ def parse(source, globals: nil, name: nil, overlay: nil, up_to_date: nil)
64
+ Template.new(
65
+ self,
66
+ source,
67
+ @parser.parse(self, source, name || "", @lexer.tokenize(self, source)),
68
+ globals: make_globals(globals),
69
+ name: name,
70
+ overlay: overlay,
71
+ up_to_date: up_to_date
72
+ )
73
+ end
74
+
75
+ # Parse and render template `source` with variables from `data`.
76
+ #
77
+ #: (String, ?t_namespace?) -> String
78
+ def render(source, data = nil)
79
+ parse(source).render(data)
80
+ end
81
+
82
+ #: (String, ?globals: _Namespace?, ?context: RenderContext?, **untyped) -> Template
83
+ def get_template(name, globals: nil, context: nil, **)
84
+ @loader.load(self, name, globals: globals, context: context, **)
85
+ end
86
+
87
+ # Add or replace a filter. The same callable can be registered multiple
88
+ # times with different names.
89
+ #
90
+ # When evaluated as a filter, _callable_ will be called with an instance of
91
+ # `FilterContext` as the first argument, and the result of the expression
92
+ # to the left of the pipe operator as the second argument.
93
+ #
94
+ # Remaining positional and keyword arguments are passed through from the
95
+ # filter invocation.
96
+ #
97
+ #: (String, untyped) -> void
98
+ def register_filter(name, callable)
99
+ @filters[name] = callable
100
+ end
101
+
102
+ # Remove a filter from the filter register.
103
+ #
104
+ #: (String) -> untyped
105
+ def delete_filter(name)
106
+ @filters.delete(name)
107
+ end
108
+
109
+ # Add or replace a tag.
110
+ #
111
+ #: (String, _Tag) -> void
112
+ def register_tag(name, tag)
113
+ @tags[name] = tag
114
+ end
115
+
116
+ # Remove a tag from the tag register.
117
+ #
118
+ #: (String) -> (_Tag | nil)
119
+ def delete_tag(name)
120
+ @tags.delete(name)
121
+ end
122
+
123
+ # Add or replace a predicate function.
124
+ #
125
+ #: (String, ^(untyped) -> bool) -> void
126
+ def register_predicate(name, callable)
127
+ @predicates[name] = callable
128
+ end
129
+
130
+ # Remove a predicate from the predicate register.
131
+ #
132
+ #: (String) -> (^(untyped) -> bool | nil)
133
+ def delete_predicate(name)
134
+ @predicates.delete(name)
135
+ end
136
+
137
+ #: () -> void
138
+ def setup_tags_filters_and_predicates
139
+ @tags["assign"] = AssignTag
140
+ @tags["capture"] = CaptureTag
141
+ @tags["case"] = CaseTag
142
+ @tags["define"] = DefineTag
143
+ @tags["for"] = ForTag
144
+ @tags["break"] = BreakTag
145
+ @tags["continue"] = ContinueTag
146
+ @tags["if"] = IfTag
147
+ @tags["import"] = ImportTag
148
+ @tags["include"] = IncludeTag
149
+ @tags["raw"] = RawTag
150
+ @tags["render"] = RenderTag
151
+ @tags["with"] = WithTag
152
+
153
+ register_filter("abs", Luoma::Filters.method(:abs))
154
+ register_filter("all", Luoma::Filters.method(:all))
155
+ register_filter("any", Luoma::Filters.method(:any))
156
+ register_filter("append", Luoma::Filters.method(:append))
157
+ register_filter("at_least", Luoma::Filters.method(:at_least))
158
+ register_filter("at_most", Luoma::Filters.method(:at_most))
159
+ register_filter("capitalize", Luoma::Filters.method(:capitalize))
160
+ register_filter("ceil", Luoma::Filters.method(:ceil))
161
+ register_filter("compact", Luoma::Filters.method(:compact))
162
+ register_filter("concat", Luoma::Filters.method(:concat))
163
+ register_filter("date", Luoma::Filters.method(:date))
164
+ register_filter("default", Luoma::Filters.method(:default))
165
+ register_filter("divided_by", Luoma::Filters.method(:divided_by))
166
+ register_filter("downcase", Luoma::Filters.method(:downcase))
167
+ register_filter("escape_once", Luoma::Filters.method(:escape_once))
168
+ register_filter("escape", Luoma::Filters.method(:escape))
169
+ register_filter("find_index", Luoma::Filters.method(:find_index))
170
+ register_filter("find", Luoma::Filters.method(:find))
171
+ register_filter("first", Luoma::Filters.method(:first))
172
+ register_filter("flatten", Luoma::Filters.method(:flatten))
173
+ register_filter("flat_map", Luoma::Filters.method(:flat_map))
174
+ register_filter("floor", Luoma::Filters.method(:floor))
175
+ register_filter("join", Luoma::Filters.method(:join))
176
+ register_filter("json", Luoma::Filters.method(:json))
177
+ register_filter("last", Luoma::Filters.method(:last))
178
+ register_filter("lstrip", Luoma::Filters.method(:lstrip))
179
+ register_filter("map", Luoma::Filters.method(:map))
180
+ register_filter("max", Luoma::Filters.method(:max))
181
+ register_filter("min", Luoma::Filters.method(:min))
182
+ register_filter("minus", Luoma::Filters.method(:minus))
183
+ register_filter("modulo", Luoma::Filters.method(:modulo))
184
+ register_filter("newline_to_br", Luoma::Filters.method(:newline_to_br))
185
+ register_filter("plus", Luoma::Filters.method(:plus))
186
+ register_filter("prepend", Luoma::Filters.method(:prepend_))
187
+ register_filter("squish", Luoma::Filters.method(:squish))
188
+ register_filter("reject", Luoma::Filters.method(:reject))
189
+ register_filter("remove_first", Luoma::Filters.method(:remove_first))
190
+ register_filter("remove_last", Luoma::Filters.method(:remove_last))
191
+ register_filter("remove", Luoma::Filters.method(:remove))
192
+ register_filter("replace_first", Luoma::Filters.method(:replace_first))
193
+ register_filter("replace_last", Luoma::Filters.method(:replace_last))
194
+ register_filter("replace", Luoma::Filters.method(:replace))
195
+ register_filter("reverse", Luoma::Filters.method(:reverse))
196
+ register_filter("round", Luoma::Filters.method(:round))
197
+ register_filter("rstrip", Luoma::Filters.method(:rstrip))
198
+ register_filter("size", Luoma::Filters.method(:size))
199
+ register_filter("slice", Luoma::Filters.method(:slice))
200
+ register_filter("sort", Luoma::Filters.method(:sort))
201
+ register_filter("sort_natural", Luoma::Filters.method(:sort_natural))
202
+ register_filter("sort_numeric", Luoma::Filters.method(:sort_numeric))
203
+ register_filter("split", Luoma::Filters.method(:split))
204
+ register_filter("strip_html", Luoma::Filters.method(:strip_html))
205
+ register_filter("strip_newlines", Luoma::Filters.method(:strip_newlines))
206
+ register_filter("strip", Luoma::Filters.method(:strip))
207
+ register_filter("sum", Luoma::Filters.method(:sum))
208
+ register_filter("times", Luoma::Filters.method(:times))
209
+ register_filter("truncate", Luoma::Filters.method(:truncate))
210
+ register_filter("truncatewords", Luoma::Filters.method(:truncatewords))
211
+ register_filter("uniq", Luoma::Filters.method(:uniq))
212
+ register_filter("upcase", Luoma::Filters.method(:upcase))
213
+ register_filter("url_decode", Luoma::Filters.method(:url_decode))
214
+ register_filter("url_encode", Luoma::Filters.method(:url_encode))
215
+ register_filter("where", Luoma::Filters.method(:where))
216
+ register_filter("filter", Luoma::Filters.method(:where))
217
+ register_filter("zip", Luoma::Filters.method(:zip))
218
+
219
+ register_predicate("array", Luoma::Predicates.method(:array?))
220
+ register_predicate("blank", Luoma::Predicates.method(:blank?))
221
+ register_predicate("defined", Luoma::Predicates.method(:defined?))
222
+ register_predicate("empty", Luoma::Predicates.method(:empty?))
223
+ register_predicate("null", Luoma::Predicates.method(:null?))
224
+ register_predicate("number", Luoma::Predicates.method(:number?))
225
+ register_predicate("numeric", Luoma::Predicates.method(:numeric?))
226
+ register_predicate("object", Luoma::Predicates.method(:object?))
227
+ register_predicate("string", Luoma::Predicates.method(:string?))
228
+ end
229
+
230
+ #: (untyped, untyped, RenderContext, t_token) -> bool
231
+ def contains?(left, right, context, token)
232
+ return left.contains?(right, context) if left.is_a?(Drop)
233
+
234
+ right = right.to_primitive(:data, context) if right.is_a?(Drop)
235
+
236
+ if left && right && left.respond_to?(:include?)
237
+ left.include?(left.is_a?(String) ? right.to_s : right)
238
+ else
239
+ false
240
+ end
241
+ end
242
+
243
+ #: (untyped, untyped, RenderContext, t_token) -> bool
244
+ def eq?(left, right, context, token)
245
+ # TODO: Don't accepts token
246
+ return left.eq?(right, context) if left.is_a?(Drop)
247
+ return right.eq?(left, context) if right.is_a?(Drop)
248
+
249
+ left == right
250
+ end
251
+
252
+ #: (untyped, untyped, RenderContext, t_token) -> bool?
253
+ def lt?(left, right, context, token)
254
+ # TODO: Don't accepts token
255
+ return left.lt?(right, context) if left.is_a?(Drop)
256
+ return right.gt?(left, context) if right.is_a?(Drop)
257
+
258
+ left < right
259
+ rescue ArgumentError, NoMethodError, TypeError
260
+ nil
261
+ end
262
+
263
+ #: (untyped, untyped, RenderContext, t_token) -> (-1 | 1 | 0 | nil)
264
+ def cmp(left, right, context, token)
265
+ return -1 if lt?(left, right, context, token)
266
+ return 1 if lt?(right, left, context, token)
267
+ return 0 if eq?(left, right, context, token)
268
+
269
+ if left.nil? || nothing?(left)
270
+ 1
271
+ elsif right.nil? || nothing?(right)
272
+ -1
273
+ end
274
+ end
275
+
276
+ #: (untyped) -> bool
277
+ def nothing?(obj)
278
+ obj == :nothing || obj.is_a?(UndefinedDrop)
279
+ end
280
+
281
+ #: (untyped, RenderContext) -> bool
282
+ def truthy?(obj, context)
283
+ obj = obj.to_primitive(:boolean, context) if obj.is_a?(Drop)
284
+ !nothing?(obj) && !!obj
285
+ end
286
+
287
+ #: (untyped, RenderContext) -> String
288
+ def serialize(obj, context)
289
+ case obj
290
+ when Drop
291
+ s = obj.render(context)
292
+ s.nil? ? to_string(obj, context) : s
293
+ when Array
294
+ if obj.all?(String)
295
+ to_string(obj, context)
296
+ elsif obj.all?(BlockDrop)
297
+ obj.map { |i| i.render(context) }.join("\n")
298
+ elsif obj.all? { |i| i.is_a?(String) || i.is_a?(BlockDrop) }
299
+ obj.map { |i| i.is_a?(String) ? i : i.render(context) }.join
300
+ else # rubocop: disable Lint/DuplicateBranch
301
+ to_string(obj, context)
302
+ end
303
+ else
304
+ to_string(obj, context)
305
+ end
306
+ end
307
+
308
+ #: (untyped, RenderContext) -> Array[untyped]
309
+ def to_a(obj, context)
310
+ if obj.is_a?(Array)
311
+ obj
312
+ elsif obj.is_a?(String)
313
+ obj.each_char.to_a
314
+ elsif obj.respond_to?(:to_a)
315
+ obj.to_a
316
+ else
317
+ [obj]
318
+ end
319
+ end
320
+
321
+ #: (untyped, RenderContext) -> Hash[untyped, untyped]
322
+ def to_h(obj, context)
323
+ if obj.is_a?(Hash)
324
+ obj
325
+ elsif obj.is_a(Drop)
326
+ obj = obj.to_primitive("object", context)
327
+ nothing?(obj) ? {} : obj
328
+ elsif obj.respond_to?(:to_h)
329
+ obj.to_h
330
+ else
331
+ {}
332
+ end
333
+ end
334
+
335
+ # Try to coerce `obj` to an integer using `#to_i` with a fallback to
336
+ # `Integer(obj.to_s)` if `obj` does not respond to `to_i`.
337
+ #
338
+ #: [X] (untyped, RenderContext, ?default: X) -> (Integer | X)
339
+ def to_i(obj, context, default: :nothing)
340
+ return obj if obj.is_a?(Integer)
341
+ return obj.to_i if obj.respond_to?(:to_i) && !obj.is_a?(String)
342
+
343
+ begin
344
+ Integer(obj.to_s)
345
+ rescue ::ArgumentError
346
+ default
347
+ end
348
+ end
349
+
350
+ #: [X] (untyped, ?default: X) -> (Numeric | X)
351
+ def to_numeric(obj, context, default: :nothing)
352
+ case obj
353
+ when ::Float
354
+ BigDecimal(obj)
355
+ when Numeric
356
+ obj
357
+ when ::String
358
+ case obj
359
+ when RE_INTEGER
360
+ obj.to_f.to_i
361
+ when RE_DECIMAL
362
+ BigDecimal(obj)
363
+ else
364
+ default
365
+ end
366
+ when true
367
+ 1
368
+ when false
369
+ 0
370
+ when Drop
371
+ n = obj.to_primitive(:numeric, context)
372
+ n == :nothing ? default : n
373
+ else
374
+ default
375
+ end
376
+ end
377
+
378
+ #: (untyped) -> Enumerable
379
+ def to_enumerable(obj, context)
380
+ case obj
381
+ when Drop
382
+ obj.each
383
+ when Enumerable
384
+ obj.to_enum
385
+ when String
386
+ obj.each_char
387
+ else
388
+ obj.respond_to?(:each) ? obj.each : [obj]
389
+ end
390
+ end
391
+
392
+ #: (untyped, RenderContext) -> String
393
+ def to_string(obj, context)
394
+ case obj
395
+ when String
396
+ obj
397
+ when Hash, Array
398
+ JSON.generate(to_data(obj, context))
399
+ when BigDecimal
400
+ # obj.to_s("F") gives higher precision
401
+ obj.to_f.to_s
402
+ when Drop
403
+ obj.to_primitive(:string, context)
404
+ when Symbol
405
+ ""
406
+ else
407
+ obj.to_s
408
+ end
409
+ end
410
+
411
+ # Cast _obj_ to a date and time. Return `nil` if casting fails.
412
+ # NOTE: This was copied from Shopify/liquid.
413
+ def to_date(obj, context)
414
+ return obj if obj.respond_to?(:strftime)
415
+
416
+ if obj.is_a?(String)
417
+ return nil if obj.empty?
418
+
419
+ obj = obj.downcase
420
+ end
421
+
422
+ case obj
423
+ when "now", "today"
424
+ Time.now
425
+ when /\A\d+\z/, Integer
426
+ Time.at(obj.to_i)
427
+ when String
428
+ Time.parse(obj)
429
+ end
430
+ rescue ::ArgumentError
431
+ nil
432
+ end
433
+
434
+ #: (String, String?, String?) -> String
435
+ def trim(value, left, right)
436
+ case left || @auto_trim
437
+ when "-"
438
+ value.lstrip!
439
+ when "~"
440
+ value.sub!(/\A[\r\n]+/, "")
441
+ end
442
+
443
+ case right
444
+ when "-"
445
+ value.rstrip!
446
+ when "~"
447
+ value.sub!(/[\r\n]+\Z/, "")
448
+ end
449
+
450
+ value
451
+ end
452
+
453
+ protected
454
+
455
+ #: (t_namespace?) -> t_namespace?
456
+ def make_globals(namespace)
457
+ namespace_ = {} #: t_namespace
458
+ namespace_.merge!(@globals) if @globals # steep:ignore
459
+ namespace_.merge!(namespace) if namespace
460
+ namespace_
461
+ end
462
+
463
+ #: (untyped, RenderContext) -> untyped
464
+ def to_data(obj, context)
465
+ case obj
466
+ when nil, true, false, Numeric, String
467
+ obj
468
+ when Array
469
+ obj.map { |v| to_data(v, context) }
470
+ when Hash
471
+ obj.transform_values { |v| to_data(v, context) }
472
+ when Drop
473
+ obj.to_primitive(:data, context)
474
+ when Symbol
475
+ ""
476
+ end
477
+ end
478
+ end
479
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Luoma
4
+ # The base class for all template errors.
5
+ class LuomaError < StandardError
6
+ end
7
+
8
+ class DetailedLuomaError < LuomaError
9
+ attr_accessor :token, :template_name, :source
10
+
11
+ def initialize(message, token, source, template_name)
12
+ super(message)
13
+ @token = token
14
+ @source = source
15
+ @template_name = template_name
16
+ end
17
+
18
+ def detailed_message(highlight: true, **kwargs)
19
+ value = Luoma.get_token_value(@token, @source)
20
+ line, col, current_line = error_context(@source, @token[1])
21
+
22
+ name_and_position = if @template_name.empty?
23
+ "#{current_line.inspect}:#{line}:#{col}"
24
+ else
25
+ "#{@template_name}:#{line}:#{col}"
26
+ end
27
+
28
+ pad = " " * line.to_s.length
29
+ pointer_size = value.empty? ? 1 : value.length
30
+ pointer = (" " * (col - 1)) + ("^" * pointer_size)
31
+
32
+ <<~MESSAGE.strip
33
+ #{self.class}: #{message}
34
+ #{pad} -> #{name_and_position}
35
+ #{pad} |
36
+ #{line} | #{current_line}
37
+ #{pad} | #{pointer} #{highlight ? "\e[1m#{message}\e[0m" : message}
38
+ MESSAGE
39
+ end
40
+
41
+ protected
42
+
43
+ def error_context(source, index)
44
+ lines = source.lines
45
+ cumulative_length = 0
46
+ target_line_index = -1
47
+
48
+ lines.each_with_index do |line, i|
49
+ cumulative_length += line.length
50
+ next unless index < cumulative_length
51
+
52
+ target_line_index = i
53
+ line_number = target_line_index + 1
54
+ column_number = index - (cumulative_length - lines[target_line_index].length) + 1
55
+ return [line_number, column_number, lines[target_line_index].rstrip]
56
+ end
57
+
58
+ # Point to end of file
59
+ line_number = lines.length
60
+ column_number = lines.last.length + 1
61
+ [line_number, column_number, lines.last.rstrip]
62
+ end
63
+ end
64
+
65
+ class DisabledTagError < DetailedLuomaError; end
66
+ class FilterArgumentError < DetailedLuomaError; end
67
+ class FilterNotFoundError < DetailedLuomaError; end
68
+ class PredicateNotFoundError < DetailedLuomaError; end
69
+ class TemplateInheritanceError < DetailedLuomaError; end
70
+ class RequiredBlockError < TemplateInheritanceError; end
71
+ class ResourceLimitError < LuomaError; end
72
+ class ContextDepthError < ResourceLimitError; end
73
+ class TemplateNotFoundError < LuomaError; end
74
+ class NoSuchTemplateError < DetailedLuomaError; end
75
+ class TemplateSyntaxError < DetailedLuomaError; end
76
+ class TemplateTypeError < DetailedLuomaError; end
77
+ class UndefinedVariableError < DetailedLuomaError; end
78
+ class IncomparableValuesError < LuomaError; end
79
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Luoma
4
+ RE_ESCAPE = /["&'<>`]/
5
+
6
+ ESCAPE_MAP = {
7
+ '"' => "&#34;",
8
+ "&" => "&amp;",
9
+ "'" => "&#39;",
10
+ "<" => "&lt;",
11
+ ">" => "&gt;",
12
+ "`" => "&#96;"
13
+ }.freeze #: Hash[String, String]
14
+
15
+ RE_UNESCAPE = /&(#34|amp|#39|lt|gt|#96);/i
16
+
17
+ UNESCAPE_MAP = {
18
+ "&#34;": '"',
19
+ "&amp;" => "&",
20
+ "&#39;" => "'",
21
+ "&lt;" => "<",
22
+ "&gt;" => ">",
23
+ "&#96;" => "`"
24
+ }.freeze #: Hash[String, String]
25
+
26
+ #: (String) -> String
27
+ def self.escape(s)
28
+ s.gsub(RE_ESCAPE) { |m| ESCAPE_MAP[m] }
29
+ end
30
+
31
+ #: (String) -> String
32
+ def self.unescape(s)
33
+ s.gsub(RE_UNESCAPE) { |m| UNESCAPE_MAP[m] }
34
+ end
35
+ end