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,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Predicates
|
|
5
|
+
#: (RenderContext, untyped) -> bool
|
|
6
|
+
def self.blank?(context, obj)
|
|
7
|
+
case obj
|
|
8
|
+
when nil
|
|
9
|
+
true
|
|
10
|
+
when String
|
|
11
|
+
obj.strip == ""
|
|
12
|
+
when Array, Hash
|
|
13
|
+
obj.empty?
|
|
14
|
+
else
|
|
15
|
+
obj.respond_to?(:length) ? obj.length.zero? : false # rubocop:disable Style/ZeroLengthPredicate
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Predicates
|
|
5
|
+
#: (RenderContext, untyped) -> bool
|
|
6
|
+
def self.empty?(context, obj)
|
|
7
|
+
case obj
|
|
8
|
+
when Array, Hash, String
|
|
9
|
+
obj.empty?
|
|
10
|
+
else
|
|
11
|
+
obj.respond_to?(:length) ? obj.length.zero? : false # rubocop:disable Style/ZeroLengthPredicate
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Predicates
|
|
5
|
+
#: (RenderContext, untyped) -> bool
|
|
6
|
+
def self.null?(context, obj)
|
|
7
|
+
obj.nil?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
#: (RenderContext, untyped) -> bool
|
|
11
|
+
def self.string?(context, obj)
|
|
12
|
+
obj.is_a?(String)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#: (RenderContext, untyped) -> bool
|
|
16
|
+
def self.number?(context, obj)
|
|
17
|
+
obj.is_a?(Numeric)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#: (RenderContext, untyped) -> bool
|
|
21
|
+
def self.array?(context, obj)
|
|
22
|
+
obj.is_a?(Array)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#: (RenderContext, untyped) -> bool
|
|
26
|
+
def self.object?(context, obj)
|
|
27
|
+
obj.is_a?(Hash)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#: (RenderContext, untyped) -> bool
|
|
31
|
+
def self.numeric?(context, obj)
|
|
32
|
+
obj.is_a?(Numeric) || (context.env.to_numeric(obj, context) != :nothing)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
# Template static analysis.
|
|
5
|
+
module StaticAnalysis
|
|
6
|
+
# The location of a variable, tag or filter.
|
|
7
|
+
class Location
|
|
8
|
+
attr_reader :template, :token
|
|
9
|
+
|
|
10
|
+
#: (Template, t_token) -> void
|
|
11
|
+
def initialize(template, token)
|
|
12
|
+
@template = template
|
|
13
|
+
@token = token
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Return the line and column number of the given index.
|
|
17
|
+
#
|
|
18
|
+
#: (Integer) -> [Integer, Integer]
|
|
19
|
+
def line_col(index)
|
|
20
|
+
lines = @template.lines
|
|
21
|
+
cumulative_length = 0
|
|
22
|
+
target_line_index = -1
|
|
23
|
+
|
|
24
|
+
lines.each_with_index do |line, i|
|
|
25
|
+
cumulative_length += line.length
|
|
26
|
+
|
|
27
|
+
if index < cumulative_length
|
|
28
|
+
target_line_index = i
|
|
29
|
+
break
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
raise LuomaError.new("index out of bounds") if target_line_index == -1
|
|
34
|
+
|
|
35
|
+
line_number = target_line_index + 1
|
|
36
|
+
line = lines[target_line_index]
|
|
37
|
+
column_number = index - (cumulative_length - line.length)
|
|
38
|
+
[line_number, column_number]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ==(other)
|
|
42
|
+
self.class == other.class &&
|
|
43
|
+
@template.name == other.template.name &&
|
|
44
|
+
@token == other.token
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias eql? ==
|
|
48
|
+
|
|
49
|
+
def hash
|
|
50
|
+
[@template.name, @token].hash
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Return the line and column number for the start and end index spanning
|
|
54
|
+
# this location.
|
|
55
|
+
#
|
|
56
|
+
#: () -> [[Integer, Integer],[Integer, Integer]]
|
|
57
|
+
def span
|
|
58
|
+
[line_col(@token[1]), line_col(@token[2])]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Return the substring spanning this location.
|
|
62
|
+
#
|
|
63
|
+
#: () -> String
|
|
64
|
+
def value
|
|
65
|
+
Luoma.get_token_value(@token, @template.source)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A variable as a sequence of segments and its location.
|
|
70
|
+
class StaticVariable
|
|
71
|
+
attr_reader :segments, :location
|
|
72
|
+
|
|
73
|
+
RE_PROPERTY = /\A[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*\Z/
|
|
74
|
+
|
|
75
|
+
def initialize(segments, location)
|
|
76
|
+
@segments = segments
|
|
77
|
+
@location = location
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def to_s
|
|
81
|
+
segments_to_s(@segments)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def ==(other)
|
|
85
|
+
self.class == other.class &&
|
|
86
|
+
@segments == other.segments
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
alias eql? ==
|
|
90
|
+
|
|
91
|
+
def hash
|
|
92
|
+
@segments.hash
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
#: () -> String
|
|
96
|
+
def root
|
|
97
|
+
segment = @segments.first || ""
|
|
98
|
+
segment.is_a?(Array) ? segments_to_s(segment) : segment.to_s
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
protected
|
|
102
|
+
|
|
103
|
+
def segments_to_s(segments)
|
|
104
|
+
head, *rest = segments
|
|
105
|
+
|
|
106
|
+
head.to_s + rest.map do |segment|
|
|
107
|
+
case segment
|
|
108
|
+
when Array
|
|
109
|
+
"[#{segments_to_s(segment)}]"
|
|
110
|
+
when String
|
|
111
|
+
if segment.match?(RE_PROPERTY) || segment.end_with?("?")
|
|
112
|
+
".#{segment}"
|
|
113
|
+
else
|
|
114
|
+
"[#{segment.inspect}]"
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
"[#{segment}]"
|
|
118
|
+
end
|
|
119
|
+
end.join
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Helper to manage variable scope during static analysis.
|
|
124
|
+
class StaticScope
|
|
125
|
+
#: (Set[String]) -> void
|
|
126
|
+
def initialize(globals)
|
|
127
|
+
@stack = [globals] #: Array[Set[String]]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def include?(key)
|
|
131
|
+
@stack.any? { |scope| scope.include?(key) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def push(scope)
|
|
135
|
+
@stack << scope
|
|
136
|
+
self
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def pop
|
|
140
|
+
@stack.pop
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def add(name)
|
|
144
|
+
@stack.first.add(name)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Helper to group variables by their root segment during static analysis.
|
|
149
|
+
class VariableMap
|
|
150
|
+
attr_reader :data
|
|
151
|
+
|
|
152
|
+
def initialize
|
|
153
|
+
@data = {}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def [](var)
|
|
157
|
+
key = var.root
|
|
158
|
+
@data[key] = [] unless @data.include?(key)
|
|
159
|
+
@data[key]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def add(var)
|
|
163
|
+
send(:[], var) << var
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
#: () -> Hash[String, Array[_Var]]
|
|
167
|
+
def to_h
|
|
168
|
+
result = {} #: Hash[String, Array[untyped]]
|
|
169
|
+
|
|
170
|
+
@data.each do |k, v|
|
|
171
|
+
a = [] #: Array[untyped]
|
|
172
|
+
|
|
173
|
+
v.each do |sv|
|
|
174
|
+
start_line, start_column, end_line, end_column = sv.location.span
|
|
175
|
+
|
|
176
|
+
a << {
|
|
177
|
+
segments: sv.segments,
|
|
178
|
+
path: sv.to_s,
|
|
179
|
+
start_index: sv.location.token[1],
|
|
180
|
+
end_index: sv.location.token[2],
|
|
181
|
+
start_line: start_line,
|
|
182
|
+
start_column: start_column,
|
|
183
|
+
end_line: end_line,
|
|
184
|
+
end_column: end_column,
|
|
185
|
+
value: sv.location.value,
|
|
186
|
+
template_name: sv.location.template.name
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
result[k] = a
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
result
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# The result of analyzing a template.
|
|
198
|
+
class Result
|
|
199
|
+
attr_reader :variables, :globals, :locals, :filters, :tags
|
|
200
|
+
|
|
201
|
+
def initialize(variables, globals, locals, filters, tags)
|
|
202
|
+
@variables = variables
|
|
203
|
+
@globals = globals
|
|
204
|
+
@locals = locals
|
|
205
|
+
@filters = filters
|
|
206
|
+
@tags = tags
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def to_h
|
|
210
|
+
{
|
|
211
|
+
variables: @variables,
|
|
212
|
+
globals: @globals,
|
|
213
|
+
locals: @locals,
|
|
214
|
+
filters: @filters,
|
|
215
|
+
tags: @tags
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def self.analyze(template, include_partials:)
|
|
221
|
+
variables = VariableMap.new
|
|
222
|
+
globals = VariableMap.new
|
|
223
|
+
locals = VariableMap.new
|
|
224
|
+
|
|
225
|
+
# @type var filters: Hash[String, Array[Location]]
|
|
226
|
+
filters = Hash.new { |hash, key| hash[key] = [] }
|
|
227
|
+
# @type var tags: Hash[String, Array[Location]]
|
|
228
|
+
tags = Hash.new { |hash, key| hash[key] = [] }
|
|
229
|
+
|
|
230
|
+
# @type var template_scope: Set[String]
|
|
231
|
+
template_scope = Set[]
|
|
232
|
+
root_scope = StaticScope.new(template_scope)
|
|
233
|
+
static_context = Luoma::RenderContext.new(template)
|
|
234
|
+
|
|
235
|
+
# Names of partial templates that have already been analyzed.
|
|
236
|
+
# Keys are hashes of partial template name and its arguments. If we've
|
|
237
|
+
# visited a template before but with different arguments, later visits
|
|
238
|
+
# only record global variables so as not to double count locals, filters
|
|
239
|
+
# and tags.
|
|
240
|
+
seen = Hash.new { |hash, key| hash[key] = Set[] } #: Hash[String,Set[untyped]]
|
|
241
|
+
|
|
242
|
+
# @type var visit: ^(Luoma::Markup, Luoma::Template, StaticScope, bool) -> void
|
|
243
|
+
visit = lambda do |node, template, scope, just_globals|
|
|
244
|
+
seen[template.name].add(nil) if !template.name.empty? && !just_globals
|
|
245
|
+
|
|
246
|
+
# Update tags
|
|
247
|
+
# Markup with empty `tag_name` is silenced.
|
|
248
|
+
tags[node.tag_name] << Location.new(template, node.token) if !just_globals && !node.tag_name.empty?
|
|
249
|
+
|
|
250
|
+
# Update variables from node.expressions
|
|
251
|
+
node.expressions.each do |expr|
|
|
252
|
+
if expr.is_a?(Luoma::Expression)
|
|
253
|
+
analyze_variables(
|
|
254
|
+
expr,
|
|
255
|
+
template,
|
|
256
|
+
scope,
|
|
257
|
+
globals,
|
|
258
|
+
just_globals ? VariableMap.new : variables,
|
|
259
|
+
static_context
|
|
260
|
+
)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
next if just_globals
|
|
264
|
+
|
|
265
|
+
# Update filters from expr
|
|
266
|
+
extract_filters(expr, template, static_context).each do |name, span|
|
|
267
|
+
filters[name] << span
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Update template scope from node.template_scope
|
|
272
|
+
node.template_scope.each do |name|
|
|
273
|
+
scope.add(name.value)
|
|
274
|
+
locals.add(StaticVariable.new([name.value], Location.new(template, name.token)))
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Set block scope before descending into child nodes.
|
|
278
|
+
scope.push(node.block_scope.to_set(&:value))
|
|
279
|
+
|
|
280
|
+
node.children(static_context).each do |child|
|
|
281
|
+
visit.call(child, template, scope, just_globals)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
scope.pop
|
|
285
|
+
|
|
286
|
+
# Descend into partial templates?
|
|
287
|
+
partial = include_partials && node.partial(static_context)
|
|
288
|
+
if partial.is_a?(Luoma::Partial)
|
|
289
|
+
name = partial.template.name
|
|
290
|
+
just_globals_ = seen.include?(name)
|
|
291
|
+
|
|
292
|
+
unless seen[name].include?(partial.key)
|
|
293
|
+
seen[name].add(partial.key)
|
|
294
|
+
|
|
295
|
+
partial_scope = if partial.scope_kind == :isolated
|
|
296
|
+
StaticScope.new(partial.in_scope.to_set(&:value))
|
|
297
|
+
else
|
|
298
|
+
root_scope.push(partial.in_scope.to_set(&:value))
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
partial.template.nodes.each do |p_node|
|
|
302
|
+
visit.call(p_node, partial.template, partial_scope, just_globals_) if p_node.is_a?(Luoma::Markup)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
partial_scope.pop if partial.scope_kind == :isolated
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
template.nodes.each do |node|
|
|
311
|
+
visit.call(node, template, root_scope, false) if node.is_a?(Luoma::Markup)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
Result.new(
|
|
315
|
+
variables.to_h,
|
|
316
|
+
globals.to_h,
|
|
317
|
+
locals.to_h,
|
|
318
|
+
to_locations(filters),
|
|
319
|
+
to_locations(tags)
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
#: (Luoma::_Traversable, Luoma::Template, Luoma::RenderContext) -> Array[[String, Location]]
|
|
324
|
+
def self.extract_filters(expression, template, static_context)
|
|
325
|
+
filters = [] # : Array[[String, Location]]
|
|
326
|
+
|
|
327
|
+
if expression.is_a?(Luoma::Filter) # steep:ignore
|
|
328
|
+
filters << [expression.name.to_s, Location.new(template, expression.span)]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
expression.children.each do |expr|
|
|
332
|
+
filters.concat(extract_filters(expr, template, static_context))
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
filters
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# (Luoma:_Traversable, Luoma::Template, StaticScope, VariableMap, VariableMap, RenderContext) -> void
|
|
339
|
+
def self.analyze_variables(expression, template, scope, globals, variables, static_context)
|
|
340
|
+
if expression.is_a?(Luoma::Variable) # steep:ignore
|
|
341
|
+
token = if expression.segments.last.is_a?(Luoma::Predicate)
|
|
342
|
+
# Don't include a the trailing predicate.
|
|
343
|
+
if expression.segments.length > 1
|
|
344
|
+
Luoma.span(expression.root.span,
|
|
345
|
+
expression.segments[-2].span)
|
|
346
|
+
else
|
|
347
|
+
expression.root.span
|
|
348
|
+
end
|
|
349
|
+
else
|
|
350
|
+
expression.span
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
var = StaticVariable.new(segments(expression, template), Location.new(template, token))
|
|
354
|
+
|
|
355
|
+
variables.add(var)
|
|
356
|
+
globals.add(var) unless scope.include?(expression.root.to_s)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# For lambda expressions.
|
|
360
|
+
child_scope = expression.is_a?(Luoma::Expression) ? expression.scope : [] # steep:ignore
|
|
361
|
+
scope.push(child_scope.to_set(&:value))
|
|
362
|
+
|
|
363
|
+
expression.children.each do |expr|
|
|
364
|
+
analyze_variables(
|
|
365
|
+
expr,
|
|
366
|
+
template,
|
|
367
|
+
scope,
|
|
368
|
+
globals,
|
|
369
|
+
variables,
|
|
370
|
+
static_context
|
|
371
|
+
)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
scope.pop
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
#: (Luoma::Variable, Luoma::Template) -> Array[untyped]
|
|
378
|
+
def self.segments(var, template)
|
|
379
|
+
segments_ = [] #: Array[untyped]
|
|
380
|
+
|
|
381
|
+
segments_ << if var.root.is_a?(Luoma::Variable)
|
|
382
|
+
segments(var.root, template)
|
|
383
|
+
else
|
|
384
|
+
var.root.value
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
var.segments.each do |s|
|
|
388
|
+
segments_ << if s.is_a?(Luoma::Variable)
|
|
389
|
+
segments(s, template)
|
|
390
|
+
elsif s.is_a?(Luoma::Predicate)
|
|
391
|
+
next
|
|
392
|
+
else
|
|
393
|
+
s.value
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
segments_
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
#: (Hash[String, Array[Location]]) -> Hash[String, Array[untyped]]
|
|
401
|
+
def self.to_locations(map)
|
|
402
|
+
result = {} #: Hash[String, Array[untyped]]
|
|
403
|
+
|
|
404
|
+
map.each do |k, v|
|
|
405
|
+
a = [] #: Array[untyped]
|
|
406
|
+
|
|
407
|
+
v.each do |l|
|
|
408
|
+
span = l.span
|
|
409
|
+
a << {
|
|
410
|
+
start_index: l.token[1],
|
|
411
|
+
end_index: l.token[2],
|
|
412
|
+
start_line: span.first.first,
|
|
413
|
+
start_column: span.first.last,
|
|
414
|
+
end_line: span.last.first,
|
|
415
|
+
end_column: span.last.last,
|
|
416
|
+
value: l.value,
|
|
417
|
+
template_name: l.template.name
|
|
418
|
+
}
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
result[k] = a
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
result
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
class AssignTag < Markup
|
|
5
|
+
attr_reader :bindings
|
|
6
|
+
|
|
7
|
+
#: (t_token, String, Parser) -> Markup
|
|
8
|
+
def self.parse(token, tag_name, parser)
|
|
9
|
+
bindings = [] #: Array[Item]
|
|
10
|
+
|
|
11
|
+
name = parser.parse_ident
|
|
12
|
+
assign_token = parser.eat(
|
|
13
|
+
:token_assign,
|
|
14
|
+
message: "bad identifier or missing assignment operator"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
bindings << Item.new(assign_token, name, parser.parse_expression)
|
|
18
|
+
|
|
19
|
+
until parser.kind != :token_comma
|
|
20
|
+
parser.next
|
|
21
|
+
name = parser.parse_ident
|
|
22
|
+
assign_token = parser.eat(
|
|
23
|
+
:token_assign,
|
|
24
|
+
message: "bad identifier or missing assignment operator"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
bindings << Item.new(assign_token, name, parser.parse_expression)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
parser.carry_whitespace_control
|
|
31
|
+
parser.eat(
|
|
32
|
+
:token_tag_end,
|
|
33
|
+
message: "bad expression or missing markup delimiter"
|
|
34
|
+
)
|
|
35
|
+
new(token, tag_name, bindings)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#: (t_token, String, Array[Item]) -> void
|
|
39
|
+
def initialize(token, tag_name, bindings)
|
|
40
|
+
super(token)
|
|
41
|
+
@blank = true
|
|
42
|
+
@tag_name = tag_name
|
|
43
|
+
@bindings = bindings
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
#: (RenderContext, String) -> void
|
|
47
|
+
def render(context, buffer)
|
|
48
|
+
@bindings.each do |binding|
|
|
49
|
+
context.assign(binding.key.value, binding.expr.evaluate(context))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#: () -> Array[Expression]
|
|
54
|
+
def expressions
|
|
55
|
+
@bindings.map(&:expr)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#: () -> Array[Name]
|
|
59
|
+
def template_scope
|
|
60
|
+
@bindings.map(&:key)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
class BreakTag < Markup
|
|
5
|
+
#: (t_token, String, Parser) -> Markup
|
|
6
|
+
def self.parse(token, tag_name, parser)
|
|
7
|
+
parser.carry_whitespace_control
|
|
8
|
+
parser.eat(:token_tag_end)
|
|
9
|
+
new(token, tag_name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(token, tag_name)
|
|
13
|
+
super(token)
|
|
14
|
+
@tag_name = tag_name
|
|
15
|
+
@blank = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#: (RenderContext, String) -> void
|
|
19
|
+
def render(context, buffer)
|
|
20
|
+
context.interrupts.push(:break)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
class CaptureTag < Markup
|
|
5
|
+
END_CAPTURE_BLOCK = Set["endcapture"]
|
|
6
|
+
|
|
7
|
+
#: (t_token, String, Parser) -> Markup
|
|
8
|
+
def self.parse(token, tag_name, parser)
|
|
9
|
+
identifier = parser.parse_ident
|
|
10
|
+
parser.carry_whitespace_control
|
|
11
|
+
parser.eat(:token_tag_end)
|
|
12
|
+
block = parser.parse_block(stop: END_CAPTURE_BLOCK)
|
|
13
|
+
parser.eat_empty_tag("endcapture")
|
|
14
|
+
new(token, tag_name, identifier, block)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: (t_token, String, Name, t_block) -> void
|
|
18
|
+
def initialize(token, tag_name, identifier, block)
|
|
19
|
+
super(token)
|
|
20
|
+
@tag_name = tag_name
|
|
21
|
+
@identifier = identifier
|
|
22
|
+
@block = block
|
|
23
|
+
@blank = true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#: (RenderContext, String) -> void
|
|
27
|
+
def render(context, buffer)
|
|
28
|
+
buf = +""
|
|
29
|
+
Luoma.render_block(@block, context, buf)
|
|
30
|
+
context.assign(@identifier.value, buf)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#: (RenderContext) -> Array[Markup]
|
|
34
|
+
def children(static_context)
|
|
35
|
+
@block.grep_v(String) #: Array[Markup]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#: () -> Array[Name]
|
|
39
|
+
def template_scope
|
|
40
|
+
[@identifier]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|