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,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
module Filters
|
|
5
|
+
INFINITY_ARRAY = [Float::INFINITY].freeze # : [Float]
|
|
6
|
+
|
|
7
|
+
# Return a sorted array of items from _left_. If _left_ contains
|
|
8
|
+
# incomparable items, return _left_ as an array with its original
|
|
9
|
+
# ordering.
|
|
10
|
+
#
|
|
11
|
+
# Coerce _left_ to an array if it's not one already.
|
|
12
|
+
def self.sort(context, left, key = nil)
|
|
13
|
+
left = context.to_a(left)
|
|
14
|
+
|
|
15
|
+
# @type var sort_: ^(Array[untyped]) ?{ (untyped, Integer) -> untyped } -> Array[untyped]
|
|
16
|
+
sort_ = lambda do |array, &key|
|
|
17
|
+
decorated = array.each_with_index.map do |item, index|
|
|
18
|
+
[item, key ? key.call(item, index) : item]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
decorated.sort! do |(_, a), (_, b)|
|
|
23
|
+
cmp = context.cmp(a, b)
|
|
24
|
+
|
|
25
|
+
case cmp
|
|
26
|
+
when nil
|
|
27
|
+
if a.nil? || context.nothing?(a)
|
|
28
|
+
1
|
|
29
|
+
elsif b.nil? || context.nothing?(b)
|
|
30
|
+
-1
|
|
31
|
+
else
|
|
32
|
+
raise IncomparableValuesError
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
cmp
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
rescue IncomparableValuesError
|
|
39
|
+
raise LuomaError.new("Cannot sort incomparable values") if context.render_context.env.strict
|
|
40
|
+
|
|
41
|
+
return array # rubocop:disable Lint/NoReturnInBeginEndBlocks
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
decorated.map!(&:first)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if key.nil? || context.nothing?(key)
|
|
48
|
+
sort_.call(left)
|
|
49
|
+
elsif key.is_a?(ExpressionDrop)
|
|
50
|
+
sort_.call(left) { |item, index| key.expr.call_with_index(item, index) }
|
|
51
|
+
else
|
|
52
|
+
key = context.to_string(key)
|
|
53
|
+
sort_.call(left) { |item, _index| context.fetch(item, key) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.sort_natural(context, left, key = :nothing)
|
|
58
|
+
left = context.to_enumerable(left)
|
|
59
|
+
|
|
60
|
+
if key.nil? || context.nothing?(key)
|
|
61
|
+
left.sort { |a, b| nil_safe_casecmp(a, b) }
|
|
62
|
+
elsif key.is_a?(ExpressionDrop)
|
|
63
|
+
key.expr.broadcast_with_index(left).zip(left).sort do |a, b|
|
|
64
|
+
nil_safe_casecmp(a.first, b.first)
|
|
65
|
+
end.map(&:last)
|
|
66
|
+
else
|
|
67
|
+
key = context.to_string(key)
|
|
68
|
+
left.sort { |a, b| nil_safe_casecmp(context.fetch(a, key), context.fetch(b, key)) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.sort_numeric(context, left, key = :nothing)
|
|
73
|
+
left = context.to_enumerable(left)
|
|
74
|
+
|
|
75
|
+
if key.nil? || context.nothing?(key)
|
|
76
|
+
left.sort { |a, b| numeric_compare(a, b, context) }
|
|
77
|
+
elsif key.is_a?(ExpressionDrop)
|
|
78
|
+
key.expr.broadcast_with_index(left).zip(left).sort do |a, b|
|
|
79
|
+
numeric_compare(a.first, b.first, context)
|
|
80
|
+
end.map(&:last)
|
|
81
|
+
else
|
|
82
|
+
key = context.to_string(key)
|
|
83
|
+
left.sort { |a, b| numeric_compare(context.fetch(a, key), context.fetch(b, key), context) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.nil_safe_casecmp(left, right)
|
|
88
|
+
if !left.nil? && !right.nil?
|
|
89
|
+
left.to_s.casecmp(right.to_s)
|
|
90
|
+
elsif left.nil? && right.nil?
|
|
91
|
+
0
|
|
92
|
+
else
|
|
93
|
+
left.nil? ? 1 : -1
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.numeric_compare(left, right, context)
|
|
98
|
+
res = ints(left, context) <=> ints(right, context)
|
|
99
|
+
res || -1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.ints(obj, context)
|
|
103
|
+
if obj.is_a?(Integer) || obj.is_a?(Float) || obj.is_a?(BigDecimal)
|
|
104
|
+
[obj]
|
|
105
|
+
else
|
|
106
|
+
numeric = context.to_string(obj).scan(/(?<=\.)0+|-?\d+/)
|
|
107
|
+
return INFINITY_ARRAY if numeric.nil? || numeric.empty?
|
|
108
|
+
|
|
109
|
+
numeric.map(&:to_i) # steep:ignore
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi/escape"
|
|
4
|
+
|
|
5
|
+
module Luoma
|
|
6
|
+
module Filters
|
|
7
|
+
# Return _left_ concatenated with _right_.
|
|
8
|
+
# Coerce _left_ and _right_ to strings if they aren't strings already.
|
|
9
|
+
def self.append(context, left, right)
|
|
10
|
+
context.to_string(left) + context.to_string(right)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Return _left_ with the first character in uppercase and the rest lowercase.
|
|
14
|
+
# Coerce _left_ to a string if it is not one already.
|
|
15
|
+
def self.capitalize(context, left)
|
|
16
|
+
context.to_string(left).capitalize
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Return _left_ with all characters converted to lowercase.
|
|
20
|
+
# Coerce _left_ to a string if it is not one already.
|
|
21
|
+
def self.downcase(context, left)
|
|
22
|
+
context.to_string(left).downcase
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Return _left_ with all characters converted to uppercase.
|
|
26
|
+
# Coerce _left_ to a string if it is not one already.
|
|
27
|
+
def self.upcase(context, left)
|
|
28
|
+
context.to_string(left).upcase
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Return _left_ with special HTML characters replaced with their HTML-safe escape sequences.
|
|
32
|
+
# Coerce _left_ to a string if it is not one already.
|
|
33
|
+
def self.escape(context, left)
|
|
34
|
+
CGI.escape_html(context.to_string(left)) unless left.nil?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Return _left_ with special HTML characters replaced with their HTML-safe escape sequences.
|
|
38
|
+
# Coerce _left_ to a string if it is not one already.
|
|
39
|
+
#
|
|
40
|
+
# It is safe to use `escape_once` on string values that already contain HTML-escape sequences.
|
|
41
|
+
def self.escape_once(context, left)
|
|
42
|
+
CGI.escape_html(CGI.unescape_html(context.to_string(left)))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Return _left_ with leading whitespace removed.
|
|
46
|
+
# Coerce _left_ to a string if it is not one already.
|
|
47
|
+
def self.lstrip(context, left)
|
|
48
|
+
context.to_string(left).lstrip
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Return _left_ with trailing whitespace removed.
|
|
52
|
+
# Coerce _left_ to a string if it is not one already.
|
|
53
|
+
def self.rstrip(context, left)
|
|
54
|
+
context.to_string(left).rstrip
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Return _left_ with leading and trailing whitespace removed.
|
|
58
|
+
# Coerce _left_ to a string if it is not one already.
|
|
59
|
+
def self.strip(context, left)
|
|
60
|
+
context.to_string(left).strip
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Return _left_ with LF or CRLF replaced with `<br />\n`.
|
|
64
|
+
def self.newline_to_br(context, left)
|
|
65
|
+
context.to_string(left).gsub(/\r?\n/, "<br />\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Return _right_ concatenated with _left_.
|
|
69
|
+
# Coerce _left_ and _right_ to strings if they aren't strings already.
|
|
70
|
+
def self.prepend_(context, left, right)
|
|
71
|
+
context.to_string(right) + context.to_string(left)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Return _left_ with all occurrences of _pattern_ replaced with _replacement_.
|
|
75
|
+
# All arguments are coerced to strings if they aren't strings already.
|
|
76
|
+
def self.replace(context, left, pattern, replacement = "")
|
|
77
|
+
context.to_string(left).gsub(context.to_string(pattern), context.to_string(replacement))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Return _left_ with the first occurrence of _pattern_ replaced with _replacement_.
|
|
81
|
+
# All arguments are coerced to strings if they aren't strings already.
|
|
82
|
+
def self.replace_first(context, left, pattern, replacement = "")
|
|
83
|
+
context.to_string(left).sub(context.to_string(pattern), context.to_string(replacement))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Return _left_ with the last occurrence of _pattern_ replaced with _replacement_.
|
|
87
|
+
# All arguments are coerced to strings if they aren't strings already.
|
|
88
|
+
def self.replace_last(context, left, pattern, replacement)
|
|
89
|
+
return left + replacement if context.nothing?(pattern)
|
|
90
|
+
|
|
91
|
+
head, match, tail = context.to_string(left).rpartition(context.to_string(pattern))
|
|
92
|
+
return left if match.empty?
|
|
93
|
+
|
|
94
|
+
head + context.to_string(replacement) + tail
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Return _left_ with all occurrences of _pattern_ removed.
|
|
98
|
+
# All arguments are coerced to strings if they aren't strings already.
|
|
99
|
+
def self.remove(context, left, pattern)
|
|
100
|
+
context.to_string(left).gsub(context.to_string(pattern), context.to_string(""))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Return _left_ with the first occurrence of _pattern_ removed.
|
|
104
|
+
# All arguments are coerced to strings if they aren't strings already.
|
|
105
|
+
def self.remove_first(context, left, pattern)
|
|
106
|
+
context.to_string(left).sub(context.to_string(pattern), context.to_string(""))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Return _left_ with the last occurrence of _pattern_ removed.
|
|
110
|
+
# All arguments are coerced to strings if they aren't strings already.
|
|
111
|
+
def self.remove_last(context, left, pattern)
|
|
112
|
+
return left if context.nothing?(pattern)
|
|
113
|
+
|
|
114
|
+
head, match, tail = context.to_string(left).rpartition(context.to_string(pattern))
|
|
115
|
+
return left if match.empty?
|
|
116
|
+
|
|
117
|
+
head + tail
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Split _left_ on every occurrence of _pattern_.
|
|
121
|
+
def self.split(context, left, pattern)
|
|
122
|
+
context.to_string(left).split(context.to_string(pattern))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
RE_HTML_BLOCKS = Regexp.union(
|
|
126
|
+
%r{<script.*?</script>}m,
|
|
127
|
+
/<!--.*?-->/m,
|
|
128
|
+
%r{<style.*?</style>}m
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
RE_HTML_TAGS = /<.*?>/m
|
|
132
|
+
|
|
133
|
+
# Return _left_ with HTML tags removed.
|
|
134
|
+
def self.strip_html(context, left)
|
|
135
|
+
context.to_string(left).gsub(RE_HTML_BLOCKS, "").gsub(RE_HTML_TAGS, "")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Return _left_ with CR and LF removed.
|
|
139
|
+
def self.strip_newlines(context, left)
|
|
140
|
+
context.to_string(left).gsub(/\r?\n/, "")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.truncate(context, left, max_length = 50, ellipsis = "...")
|
|
144
|
+
return if left.nil? || context.nothing?(left)
|
|
145
|
+
|
|
146
|
+
left = context.to_string(left)
|
|
147
|
+
max_length = context.to_i(max_length)
|
|
148
|
+
return left if left.length <= max_length
|
|
149
|
+
|
|
150
|
+
ellipsis = context.to_string(ellipsis)
|
|
151
|
+
return ellipsis[0, max_length] if ellipsis.length >= max_length
|
|
152
|
+
|
|
153
|
+
"#{left[0...(max_length - ellipsis.length)]}#{ellipsis}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.truncatewords(context, left, max_words = 15, ellipsis = "...")
|
|
157
|
+
return if left.nil? || context.nothing?(left)
|
|
158
|
+
|
|
159
|
+
left = context.to_string(left)
|
|
160
|
+
max_words = context.to_i(max_words).clamp(1, 10_000)
|
|
161
|
+
words = left.split(" ", max_words + 1)
|
|
162
|
+
return left if words.length <= max_words
|
|
163
|
+
|
|
164
|
+
ellipsis = context.to_string(ellipsis)
|
|
165
|
+
words.pop
|
|
166
|
+
"#{words.join(" ")}#{ellipsis}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.url_encode(context, left)
|
|
170
|
+
CGI.escape(context.to_string(left)) unless left.nil? || context.nothing?(left)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.url_decode(context, left)
|
|
174
|
+
return if left.nil? || context.nothing?(left)
|
|
175
|
+
|
|
176
|
+
decoded = CGI.unescape(context.to_string(left))
|
|
177
|
+
raise context.argument_error("invalid byte sequence") unless decoded.valid_encoding?
|
|
178
|
+
|
|
179
|
+
decoded
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.squish(context, left)
|
|
183
|
+
context.to_string(left).strip.gsub(/\s+/, " ") unless left.nil?
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
data/lib/luoma/fnv.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
def self.fnv1a32(s)
|
|
5
|
+
hash = 0x811c9dc5 # offset basis for 32 bit hash
|
|
6
|
+
|
|
7
|
+
s.bytes do |b|
|
|
8
|
+
hash ^= b
|
|
9
|
+
hash *= 0x01000193 # FNV prime for 32 bit hash
|
|
10
|
+
hash &= (1 << 32) - 1
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
hash
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/luoma/lexer.rb
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
module Luoma
|
|
6
|
+
# The base class for template source code tokenizers.
|
|
7
|
+
class BaseLexer
|
|
8
|
+
attr_reader :tokens
|
|
9
|
+
|
|
10
|
+
#: (Environment, String) -> Array[t_token]
|
|
11
|
+
def self.tokenize(env, source)
|
|
12
|
+
lexer = new(env, source)
|
|
13
|
+
lexer.go
|
|
14
|
+
lexer.tokens
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: (Environment, String) -> void
|
|
18
|
+
def initialize(env, source)
|
|
19
|
+
@env = env
|
|
20
|
+
@source = source
|
|
21
|
+
@scanner = StringScanner.new(source)
|
|
22
|
+
@start = 0
|
|
23
|
+
@tokens = [] #: Array[t_token]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#: () -> void
|
|
27
|
+
def go
|
|
28
|
+
state = :scan_markup
|
|
29
|
+
state = send(state) until state.nil?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
protected
|
|
33
|
+
|
|
34
|
+
# Scanner entry point. If a symbol is returned it should match the name of
|
|
35
|
+
# a method implementing the next state. A return value of `nil` means we
|
|
36
|
+
# should stop scanning.
|
|
37
|
+
#: () -> Symbol?
|
|
38
|
+
def scan_markup
|
|
39
|
+
raise "not implemented"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Emit a token of `kind` spanning @start to @scanner.pos.
|
|
43
|
+
#: (t_token_kind) -> void
|
|
44
|
+
def emit(kind)
|
|
45
|
+
# @tokens << { kind: kind, start: @start, stop: @scanner.pos - 1 }
|
|
46
|
+
@tokens << [kind, @start, @scanner.pos - 1]
|
|
47
|
+
@start = @scanner.pos
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Return the start index of the next match of `pattern` without advancing
|
|
51
|
+
# the scanner.
|
|
52
|
+
#
|
|
53
|
+
# It's faster to use `@scanner.string.byteindex(s, @scanner.pos)` if you
|
|
54
|
+
# are searching for a string literal instead of a pattern.
|
|
55
|
+
#
|
|
56
|
+
#: (Regexp) -> Integer?
|
|
57
|
+
def index(pattern)
|
|
58
|
+
byte_offset = @scanner.exist?(pattern)
|
|
59
|
+
return nil unless byte_offset
|
|
60
|
+
|
|
61
|
+
@scanner.pos + byte_offset - (@scanner.matched_size || raise)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#: (Regexp) -> String?
|
|
65
|
+
def scan(pattern)
|
|
66
|
+
# NOTE: calling @scanner.scan directly yields a significant increase in
|
|
67
|
+
# performance
|
|
68
|
+
@scanner.scan(pattern)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Scan up to but not including `pattern`.
|
|
72
|
+
#: (Regexp) -> bool
|
|
73
|
+
def scan_until?(pattern)
|
|
74
|
+
byte_offset = @scanner.exist?(pattern)
|
|
75
|
+
if byte_offset.nil?
|
|
76
|
+
false
|
|
77
|
+
else
|
|
78
|
+
@scanner.pos += byte_offset - (@scanner.matched_size || raise)
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
#: (Regexp) -> bool
|
|
84
|
+
def skip?(pattern)
|
|
85
|
+
if @scanner.scan(pattern)
|
|
86
|
+
@start = @scanner.pos
|
|
87
|
+
true
|
|
88
|
+
else
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Skip up to but not including `pattern`.
|
|
94
|
+
#: (Regexp) -> bool
|
|
95
|
+
def skip_until?(pattern)
|
|
96
|
+
byte_offset = @scanner.exist?(pattern)
|
|
97
|
+
if byte_offset.nil?
|
|
98
|
+
false
|
|
99
|
+
else
|
|
100
|
+
@scanner.pos += byte_offset - (@scanner.matched_size || raise)
|
|
101
|
+
@start = @scanner.pos
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|