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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +2 -0
- data/.rdoc_options +16 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +68 -0
- data/Rakefile +69 -0
- data/Steepfile +41 -0
- data/certs/jgrp.pem +27 -0
- data/docs/configuration.md +180 -0
- data/docs/custom_filters.md +3 -0
- data/docs/custom_tags.md +3 -0
- data/docs/expressions.md +3 -0
- data/docs/extension_types.md +41 -0
- data/docs/filter_reference.md +1702 -0
- data/docs/index.md +73 -0
- data/docs/luoma_for_template_authors.md +3 -0
- data/docs/markdown.md +111 -0
- data/docs/predicate_reference.md +3 -0
- data/docs/static_analysis.md +3 -0
- data/docs/tag_reference.md +352 -0
- data/docs/template_loaders.md +177 -0
- data/docs/undefined_variables.md +3 -0
- data/docs-requirements.txt +11 -0
- data/docs_/cycle.md +17 -0
- data/docs_/font_rendering_example.md +157 -0
- data/docs_/header_block_example.md +51 -0
- data/docs_/increment_and_decrement.md +49 -0
- data/docs_/logo_style_example.md +40 -0
- data/docs_/migration.md +11 -0
- data/docs_/notes.md +19 -0
- data/lib/luoma/cache.rb +80 -0
- data/lib/luoma/chain_hash.rb +54 -0
- data/lib/luoma/context.rb +227 -0
- data/lib/luoma/drop.rb +84 -0
- data/lib/luoma/drops/block.rb +33 -0
- data/lib/luoma/drops/expression.rb +29 -0
- data/lib/luoma/drops/html_safe.rb +36 -0
- data/lib/luoma/drops/range.rb +69 -0
- data/lib/luoma/drops/undefined.rb +119 -0
- data/lib/luoma/environment.rb +479 -0
- data/lib/luoma/errors.rb +79 -0
- data/lib/luoma/escape.rb +35 -0
- data/lib/luoma/expression.rb +1271 -0
- data/lib/luoma/filter.rb +97 -0
- data/lib/luoma/filters/array.rb +372 -0
- data/lib/luoma/filters/date.rb +19 -0
- data/lib/luoma/filters/default.rb +16 -0
- data/lib/luoma/filters/json.rb +14 -0
- data/lib/luoma/filters/math.rb +84 -0
- data/lib/luoma/filters/size.rb +13 -0
- data/lib/luoma/filters/slice.rb +52 -0
- data/lib/luoma/filters/sort.rb +113 -0
- data/lib/luoma/filters/string.rb +186 -0
- data/lib/luoma/fnv.rb +15 -0
- data/lib/luoma/lexer.rb +106 -0
- data/lib/luoma/lexer_legacy.rb +396 -0
- data/lib/luoma/lexer_unified.rb +279 -0
- data/lib/luoma/loader.rb +50 -0
- data/lib/luoma/loaders/choice_loader.rb +20 -0
- data/lib/luoma/loaders/file_system_loader.rb +75 -0
- data/lib/luoma/loaders/mixins.rb +52 -0
- data/lib/luoma/markup.rb +109 -0
- data/lib/luoma/parser.rb +252 -0
- data/lib/luoma/parser_unified.rb +979 -0
- data/lib/luoma/predicates/blank.rb +19 -0
- data/lib/luoma/predicates/defined.rb +10 -0
- data/lib/luoma/predicates/empty.rb +15 -0
- data/lib/luoma/predicates/type.rb +35 -0
- data/lib/luoma/static_analysis.rb +427 -0
- data/lib/luoma/tags/assign.rb +63 -0
- data/lib/luoma/tags/break.rb +23 -0
- data/lib/luoma/tags/capture.rb +43 -0
- data/lib/luoma/tags/case.rb +137 -0
- data/lib/luoma/tags/comment.rb +17 -0
- data/lib/luoma/tags/continue.rb +23 -0
- data/lib/luoma/tags/define.rb +42 -0
- data/lib/luoma/tags/else.rb +30 -0
- data/lib/luoma/tags/for.rb +108 -0
- data/lib/luoma/tags/if.rb +109 -0
- data/lib/luoma/tags/import.rb +91 -0
- data/lib/luoma/tags/include.rb +77 -0
- data/lib/luoma/tags/output.rb +20 -0
- data/lib/luoma/tags/raw.rb +26 -0
- data/lib/luoma/tags/render.rb +92 -0
- data/lib/luoma/tags/with.rb +55 -0
- data/lib/luoma/template.rb +178 -0
- data/lib/luoma/token.rb +84 -0
- data/lib/luoma/version.rb +5 -0
- data/lib/luoma.rb +75 -0
- data/sig/luoma/cache.rbs +44 -0
- data/sig/luoma/chain_hash.rbs +26 -0
- data/sig/luoma/context.rbs +115 -0
- data/sig/luoma/drop.rbs +44 -0
- data/sig/luoma/drops/block.rbs +10 -0
- data/sig/luoma/drops/expression.rbs +10 -0
- data/sig/luoma/drops/html_safe.rbs +14 -0
- data/sig/luoma/drops/range.rbs +15 -0
- data/sig/luoma/drops/undefined.rbs +27 -0
- data/sig/luoma/environment.rbs +212 -0
- data/sig/luoma/errors.rbs +67 -0
- data/sig/luoma/escape.rbs +15 -0
- data/sig/luoma/expression.rbs +514 -0
- data/sig/luoma/filter.rbs +66 -0
- data/sig/luoma/filters/array.rbs +74 -0
- data/sig/luoma/filters/date.rbs +9 -0
- data/sig/luoma/filters/default.rbs +8 -0
- data/sig/luoma/filters/json.rbs +7 -0
- data/sig/luoma/filters/math.rbs +37 -0
- data/sig/luoma/filters/size.rbs +6 -0
- data/sig/luoma/filters/slice.rbs +10 -0
- data/sig/luoma/filters/sort.rbs +17 -0
- data/sig/luoma/filters/string.rbs +103 -0
- data/sig/luoma/fnv.rbs +3 -0
- data/sig/luoma/lexer.rbs +42 -0
- data/sig/luoma/lexer_legacy.rbs +81 -0
- data/sig/luoma/lexer_unified.rbs +49 -0
- data/sig/luoma/loader.rbs +40 -0
- data/sig/luoma/loaders/choice_loader.rbs +9 -0
- data/sig/luoma/loaders/file_system_loader.rbs +19 -0
- data/sig/luoma/loaders/mixins.rbs +16 -0
- data/sig/luoma/markup.rbs +68 -0
- data/sig/luoma/parser.rbs +105 -0
- data/sig/luoma/parser_unified.rbs +150 -0
- data/sig/luoma/predicates/blank.rbs +6 -0
- data/sig/luoma/predicates/defined.rbs +6 -0
- data/sig/luoma/predicates/empty.rbs +6 -0
- data/sig/luoma/predicates/type.rbs +21 -0
- data/sig/luoma/static_analysis.rbs +141 -0
- data/sig/luoma/tags/assign.rbs +12 -0
- data/sig/luoma/tags/break.rbs +11 -0
- data/sig/luoma/tags/capture.rbs +14 -0
- data/sig/luoma/tags/case.rbs +36 -0
- data/sig/luoma/tags/comment.rbs +11 -0
- data/sig/luoma/tags/continue.rbs +11 -0
- data/sig/luoma/tags/define.rbs +16 -0
- data/sig/luoma/tags/else.rbs +17 -0
- data/sig/luoma/tags/for.rbs +26 -0
- data/sig/luoma/tags/if.rbs +45 -0
- data/sig/luoma/tags/import.rbs +13 -0
- data/sig/luoma/tags/include.rbs +13 -0
- data/sig/luoma/tags/output.rbs +13 -0
- data/sig/luoma/tags/raw.rbs +11 -0
- data/sig/luoma/tags/render.rbs +13 -0
- data/sig/luoma/tags/with.rbs +15 -0
- data/sig/luoma/template.rbs +117 -0
- data/sig/luoma/token.rbs +81 -0
- data/sig/luoma.rbs +11 -0
- data/zensical.toml +363 -0
- data.tar.gz.sig +0 -0
- metadata +233 -0
- 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
|
data/lib/luoma/errors.rb
ADDED
|
@@ -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
|
data/lib/luoma/escape.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
RE_ESCAPE = /["&'<>`]/
|
|
5
|
+
|
|
6
|
+
ESCAPE_MAP = {
|
|
7
|
+
'"' => """,
|
|
8
|
+
"&" => "&",
|
|
9
|
+
"'" => "'",
|
|
10
|
+
"<" => "<",
|
|
11
|
+
">" => ">",
|
|
12
|
+
"`" => "`"
|
|
13
|
+
}.freeze #: Hash[String, String]
|
|
14
|
+
|
|
15
|
+
RE_UNESCAPE = /&(#34|amp|#39|lt|gt|#96);/i
|
|
16
|
+
|
|
17
|
+
UNESCAPE_MAP = {
|
|
18
|
+
""": '"',
|
|
19
|
+
"&" => "&",
|
|
20
|
+
"'" => "'",
|
|
21
|
+
"<" => "<",
|
|
22
|
+
">" => ">",
|
|
23
|
+
"`" => "`"
|
|
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
|