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,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Luoma
4
+ # The base class for all template loaders.
5
+ class TemplateLoader
6
+ # Load and return template source text and any associated data.
7
+ #: (Environment, String, ?context: RenderContext?, **untyped) -> TemplateSource
8
+ def get_source(env, name, context: nil, **kwargs)
9
+ raise "not implemented"
10
+ end
11
+
12
+ #: (Environment, String, ?globals: t_namespace?, ?context: RenderContext?, **kwargs) -> Template
13
+ def load(env, name, globals: nil, context: nil, **kwargs)
14
+ data = get_source(env, name, context: context, **kwargs)
15
+ env.parse(data.source,
16
+ name: data.name,
17
+ globals: globals,
18
+ up_to_date: data.up_to_date,
19
+ overlay: data.matter)
20
+ end
21
+ end
22
+
23
+ # Template source text and meta data.
24
+ class TemplateSource
25
+ attr_accessor :source, :name, :up_to_date, :matter
26
+
27
+ def initialize(source:, name:, up_to_date: nil, matter: nil)
28
+ @source = source
29
+ @name = name
30
+ @up_to_date = up_to_date
31
+ @matter = matter
32
+ end
33
+ end
34
+
35
+ # A template loader that reads templates from a hash.
36
+ class HashLoader < TemplateLoader
37
+ #: (Hash<String, String>) -> void
38
+ def initialize(templates)
39
+ super()
40
+ @templates = templates
41
+ end
42
+
43
+ def get_source(env, name, context: nil, **kwargs)
44
+ source = @templates[name]
45
+ raise TemplateNotFoundError.new("template not found #{name}") unless source
46
+
47
+ TemplateSource.new(source: source, name: name)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Luoma
4
+ class ChoiceLoader < TemplateLoader
5
+ def initialize(*loaders)
6
+ super
7
+ @loaders = loaders
8
+ end
9
+
10
+ def load(env, name, **kwargs)
11
+ @loaders.each do |loader|
12
+ return loader.load(env, name, **kwargs)
13
+ rescue TemplateNotFoundError
14
+ next
15
+ end
16
+
17
+ raise TemplateNotFoundError.new("template not found #{name.inspect}")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Luoma
6
+ # A template loader that reads template from a file system.
7
+ class FileSystemLoader < TemplateLoader
8
+ def initialize(search_path, default_extension: nil)
9
+ super()
10
+ @search_path = if search_path.is_a?(Array)
11
+ search_path.map { |p| Pathname.new(p) }
12
+ else
13
+ [Pathname.new(search_path)]
14
+ end
15
+
16
+ @default_extension = default_extension
17
+ end
18
+
19
+ def get_source(_env, name, context: nil, **_kwargs)
20
+ path = resolve_path(name)
21
+ mtime = path.mtime
22
+ up_to_date = -> { path.mtime == mtime }
23
+ TemplateSource.new(source: path.read, name: path.basename.to_s, up_to_date: up_to_date)
24
+ end
25
+
26
+ def resolve_path(template_name)
27
+ template_path = Pathname.new(template_name)
28
+
29
+ # Append the default file extension if needed.
30
+ if @default_extension && template_path.extname.empty?
31
+ template_path = template_path.sub_ext(@default_extension || raise)
32
+ end
33
+
34
+ # TODO: better relative path check
35
+
36
+ # Don't alow template names to escape the search path with "../".
37
+ template_path.each_filename do |part|
38
+ next unless part == ".."
39
+
40
+ raise TemplateNotFoundError.new("template not found #{template_name}")
41
+ end
42
+
43
+ # Search each path in turn.
44
+ @search_path.each do |path|
45
+ source_path = path.join(template_path)
46
+ return source_path if source_path.file?
47
+ end
48
+
49
+ raise TemplateNotFoundError.new("template not found #{template_name}")
50
+ end
51
+ end
52
+
53
+ # A file system template loader that caches parsed templates.
54
+ class CachingFileSystemLoader < FileSystemLoader
55
+ include CachingLoaderMixin
56
+
57
+ def initialize(
58
+ search_path,
59
+ default_extension: nil,
60
+ auto_reload: true,
61
+ namespace_key: "",
62
+ capacity: 300,
63
+ thread_safe: false
64
+ )
65
+ super(search_path, default_extension: default_extension)
66
+
67
+ initialize_cache(
68
+ auto_reload: auto_reload,
69
+ namespace_key: namespace_key,
70
+ capacity: capacity,
71
+ thread_safe: thread_safe
72
+ )
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cache"
4
+
5
+ module Luoma
6
+ # A mixin that adds caching to a template loader.
7
+ module CachingLoaderMixin
8
+ def initialize_cache(auto_reload: true, namespace_key: "", capacity: 300, thread_safe: false)
9
+ @auto_reload = auto_reload
10
+ @namespace_key = namespace_key
11
+ @cache = if thread_safe
12
+ ThreadSafeLRUCache.new(capacity)
13
+ else
14
+ LRUCache.new(capacity)
15
+ end
16
+ end
17
+
18
+ def load(env, name, globals: nil, context: nil, **)
19
+ key = cache_key(name, context: context, **)
20
+
21
+ # @type var template: Template
22
+ # @type var cached_template: Template
23
+ if (cached_template = @cache[key])
24
+ if @auto_reload && cached_template.up_to_date? == false
25
+ template = super
26
+ @cache[key] = template
27
+ template
28
+ else
29
+ cached_template
30
+ end
31
+ else
32
+ template = super
33
+ @cache[key] = template
34
+ template
35
+ end
36
+ end
37
+
38
+ def cache_key(name, context: nil, **kwargs)
39
+ return name unless @namespace_key
40
+
41
+ key = (@namespace_key || raise).to_sym
42
+ return "#{kwargs[key]}/#{name}" if kwargs.include?(key)
43
+ return name unless context
44
+
45
+ if (namespace = context.globals[@namespace_key || raise])
46
+ "#{namespace}/#{name}"
47
+ else
48
+ name
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Luoma
4
+ # The base class for all tag nodes and the output statement.
5
+ class Markup
6
+ attr_reader :token, :blank, :tag_name
7
+
8
+ #: (t_token) -> void
9
+ def initialize(token)
10
+ @token = token
11
+ @blank = false
12
+ @tag_name = ""
13
+ end
14
+
15
+ #: (RenderContext, String) -> void
16
+ def render(context, buffer)
17
+ raise "not implemented"
18
+ end
19
+
20
+ #: (RenderContext) -> Array[Markup]
21
+ def children(static_context)
22
+ []
23
+ end
24
+
25
+ #: () -> Array[Expression]
26
+ def expressions
27
+ []
28
+ end
29
+
30
+ #: () -> Array[Name]
31
+ def block_scope
32
+ []
33
+ end
34
+
35
+ #: () -> Array[Name]
36
+ def template_scope
37
+ []
38
+ end
39
+
40
+ #: (RenderContext) -> Partial?
41
+ def partial(static_context)
42
+ nil
43
+ end
44
+ end
45
+
46
+ class Partial
47
+ attr_reader :template, :scope_kind, :in_scope, :key
48
+
49
+ #: (Template, :shared | :isolated | :inherited, Array[Name], Integer) -> void
50
+ def initialize(template, scope_kind, in_scope, key)
51
+ @template = template
52
+ @scope_kind = scope_kind
53
+ @in_scope = in_scope
54
+ @key = key
55
+ end
56
+ end
57
+
58
+ #: (t_block, RenderContext, String, ?root: false) -> void
59
+ def self.render_block(block, context, buffer, root: false)
60
+ if context.env.max_render_score || context.env.max_render_score_cumulative
61
+ context.render_score += block.length
62
+ context.render_score_cumulative += block.length
63
+
64
+ if (context.env.max_render_score && context.render_score > context.env.max_render_score) ||
65
+ (context.env.max_render_score_cumulative &&
66
+ context.render_score_cumulative > context.env.max_render_score_cumulative)
67
+ raise ResourceLimitError.new("memory limits reached")
68
+ end
69
+ end
70
+
71
+ block.each do |node|
72
+ if node.is_a?(String)
73
+ buffer << node
74
+ else
75
+ if context.disabled_tags&.include?(node.tag_name)
76
+ raise DisabledTagError.new(
77
+ "#{node.tag_name.inspect} is not allowed in this context",
78
+ node.token,
79
+ context.template.source,
80
+ context.template.name
81
+ )
82
+ end
83
+
84
+ node.render(context, buffer)
85
+
86
+ # TODO: disable these when strict is false?
87
+ if root && !context.interrupts.empty?
88
+ raise TemplateSyntaxError.new(
89
+ "unexpected #{context.interrupts.last}",
90
+ node.token,
91
+ context.template.source,
92
+ context.template.name
93
+ )
94
+ end
95
+ end
96
+
97
+ if context.env.max_render_size && buffer.bytesize > context.env.max_render_size
98
+ raise ResourceLimitError.new("memory limits exceeded")
99
+ end
100
+
101
+ break unless context.interrupts.empty?
102
+ end
103
+ end
104
+
105
+ #: (t_block) -> bool
106
+ def self.blank_block?(block)
107
+ block.all? { |node| node.is_a?(String) ? node.match?(/\A\s*\Z/) : node.blank }
108
+ end
109
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Luoma
4
+ class Parser
5
+ attr_reader :source, :template_name, :env
6
+
7
+ TERMINATE_EXPRESSION = Set.new(%i[
8
+ token_wc
9
+ token_out_end
10
+ token_tag_end
11
+ token_text
12
+ token_rparen
13
+ token_eoi
14
+ token_if
15
+ token_else
16
+ token_interpolation_end
17
+ ]).freeze #: Set[t_token_kind]
18
+
19
+ #: (Environment, String, String, Array[t_token]) -> t_block
20
+ def self.parse(env, source, template_name, tokens)
21
+ new(env, source, template_name, tokens).parse_block
22
+ end
23
+
24
+ #: (Environment, String, String, Array[t_token]) -> void
25
+ def initialize(env, source, template_name, tokens)
26
+ @env = env
27
+ @source = source
28
+ @template_name = template_name
29
+ @tokens = tokens
30
+
31
+ @pos = 0
32
+ @eoi = [:token_eoi, source.length, source.length]
33
+ @whitespace_control_carry = nil #: "-" | "+" | "~" | nil
34
+ end
35
+
36
+ #: () -> void
37
+ def carry_whitespace_control
38
+ @whitespace_control_carry = (Luoma.get_token_value(self.next, @source) if kind == :token_wc)
39
+ end
40
+
41
+ #: () -> t_token
42
+ def current
43
+ @tokens[@pos] || @eoi
44
+ end
45
+
46
+ #: () -> t_token_kind
47
+ def kind
48
+ (@tokens[@pos] || @eoi).first #: t_token_kind
49
+ end
50
+
51
+ #: () -> String
52
+ def current_value
53
+ Luoma.get_token_value(@tokens[@pos] || @eoi, @source)
54
+ end
55
+
56
+ #: () -> t_token
57
+ def next
58
+ token = @tokens[@pos] || @eoi
59
+ @pos += 1
60
+ token
61
+ end
62
+
63
+ def peek(offset = 1)
64
+ @tokens[@pos + offset] || @eoi
65
+ end
66
+
67
+ #: (t_token_kind, ?message: String?) -> t_token
68
+ def eat(kind, message: nil)
69
+ token = @tokens[@pos] || @eoi
70
+ unless token.first == kind
71
+ kind_ = Luoma::TOKEN_KIND_MAP[token.first]
72
+ value = Luoma.get_token_value(token, @source).inspect
73
+ raise TemplateSyntaxError.new(
74
+ message || "unexpected #{kind_} (#{value})",
75
+ token,
76
+ @source,
77
+ @template_name
78
+ )
79
+ end
80
+
81
+ @pos += 1
82
+ token
83
+ end
84
+
85
+ #: (String) -> t_token
86
+ def eat_empty_tag(name)
87
+ eat(:token_tag_start, message: "expected tag #{name}")
88
+ @pos += 1 if kind == :token_wc # steep:ignore
89
+ token = eat(:token_tag_name, message: "expected tag #{name}")
90
+ got = Luoma.get_token_value(token, @source)
91
+
92
+ unless got == name
93
+ raise TemplateSyntaxError.new(
94
+ "unexpected tag #{got.inspect}",
95
+ token,
96
+ @source,
97
+ @template_name
98
+ )
99
+ end
100
+
101
+ carry_whitespace_control
102
+ eat(:token_tag_end, message: "expected a closing tag delimiter")
103
+ token
104
+ end
105
+
106
+ #: (Set[t_token_kind]) -> t_token
107
+ def eat_one_of(kinds)
108
+ token = @tokens[@pos] || @eoi
109
+ unless kinds.include?(token.first)
110
+ kind_ = Luoma::TOKEN_KIND_MAP[token.first]
111
+ value = Luoma.get_token_value(token, @source).inspect
112
+ raise TemplateSyntaxError.new(
113
+ "unexpected #{kind_} (#{value})",
114
+ token,
115
+ @source,
116
+ @template_name
117
+ )
118
+ end
119
+
120
+ @pos += 1
121
+ token
122
+ end
123
+
124
+ #: (String) -> t_token
125
+ def eat_tag(name)
126
+ eat(:token_tag_start, message: "expected tag #{name}")
127
+ @pos += 1 if kind == :token_wc # steep:ignore
128
+ token = eat(:token_tag_name, message: "expected tag #{name}")
129
+ got = Luoma.get_token_value(token, @source)
130
+
131
+ unless got == name
132
+ raise TemplateSyntaxError.new(
133
+ "unexpected tag #{got.inspect}",
134
+ token,
135
+ @source,
136
+ @template_name
137
+ )
138
+ end
139
+
140
+ # Ignore everything between the tag name and the closing tag delimiter.
141
+ @pos += 1 until TERMINATE_EXPRESSION.include?(kind)
142
+
143
+ carry_whitespace_control
144
+ eat(:token_tag_end, message: "expected a closing tag delimiter")
145
+ token
146
+ end
147
+
148
+ # Raise an error if we're not at the start of an expression.
149
+ #: () -> void
150
+ def expect_expression
151
+ if TERMINATE_EXPRESSION.include?(kind)
152
+ raise TemplateSyntaxError.new(
153
+ "missing expression",
154
+ current,
155
+ @source,
156
+ @template_name
157
+ )
158
+ end
159
+ end
160
+
161
+ #: (?require_commas: bool?) -> Array[Expression | KeywordArgument]
162
+ def parse_arguments(require_commas: nil)
163
+ raise "not implemented"
164
+ end
165
+
166
+ #: (?stop: Set[String]?) -> t_block
167
+ def parse_block(stop: nil)
168
+ raise "not implemented"
169
+ end
170
+
171
+ #: (?precedence: Integer) -> Expression
172
+ def parse_expression(precedence: 1)
173
+ raise "not implemented"
174
+ end
175
+
176
+ #: () -> Name
177
+ def parse_ident
178
+ raise "not implemented"
179
+ end
180
+
181
+ #: (?require_commas: bool?) -> Array[KeywordArgument]
182
+ def parse_keyword_arguments(require_commas: nil)
183
+ raise "not implemented"
184
+ end
185
+
186
+ # Parse an identifier, possibly surrounded by quotes.
187
+ #: () -> Name
188
+ def parse_name
189
+ raise "not implemented"
190
+ end
191
+
192
+ #: (?require_commas: bool?) -> Array[Expression]
193
+ def parse_positional_arguments(require_commas: nil)
194
+ raise "not implemented"
195
+ end
196
+
197
+ #: () -> StringLiteral
198
+ def parse_string_literal
199
+ raise "not implemented"
200
+ end
201
+
202
+ #: () -> String
203
+ def peek_tag_name
204
+ token = current
205
+ token = peek if token.first == :token_wc
206
+
207
+ unless token.first == :token_tag_name
208
+ raise TemplateSyntaxError.new(
209
+ "missing tag name",
210
+ token,
211
+ @source,
212
+ @template_name
213
+ )
214
+ end
215
+
216
+ Luoma.get_token_value(token, @source)
217
+ end
218
+
219
+ #: () -> String?
220
+ def peek_whitespace_control
221
+ token = peek
222
+ Luoma.get_token_value(token, @source) if token.first == :token_wc
223
+ end
224
+
225
+ #: () -> void
226
+ def skip_whitespace_control
227
+ @pos += 1 if kind == :token_wc
228
+ end
229
+
230
+ # Return `true` if we're at the start of a tag named `name`.
231
+ #: (String) -> bool
232
+ def tag?(name)
233
+ token = peek
234
+ token = peek(2) if token.first == :token_wc
235
+
236
+ token.first == :token_tag_name && Luoma.get_token_value(token, @source) == name
237
+ end
238
+
239
+ # Return a tag name if we're at the start of a tag and that tag's name is
240
+ # in `names`.
241
+ #: (Set[String]) -> String?
242
+ def tags(names)
243
+ token = peek
244
+ token = peek(2) if token.first == :token_wc
245
+
246
+ return unless token.first == :token_tag_name
247
+
248
+ name = Luoma.get_token_value(token, @source)
249
+ name if names.include?(name)
250
+ end
251
+ end
252
+ end