foxtail-tools 0.5.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
- data/CHANGELOG.md +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +66 -0
- data/exe/foxtail +12 -0
- data/lib/foxtail/cli/commands/check.rb +60 -0
- data/lib/foxtail/cli/commands/dump.rb +43 -0
- data/lib/foxtail/cli/commands/ids.rb +73 -0
- data/lib/foxtail/cli/commands/tidy.rb +107 -0
- data/lib/foxtail/cli.rb +59 -0
- data/lib/foxtail/syntax/error.rb +8 -0
- data/lib/foxtail/syntax/parser/ast/annotation.rb +23 -0
- data/lib/foxtail/syntax/parser/ast/attribute.rb +23 -0
- data/lib/foxtail/syntax/parser/ast/base_comment.rb +19 -0
- data/lib/foxtail/syntax/parser/ast/base_literal.rb +24 -0
- data/lib/foxtail/syntax/parser/ast/base_node.rb +89 -0
- data/lib/foxtail/syntax/parser/ast/call_arguments.rb +23 -0
- data/lib/foxtail/syntax/parser/ast/comment.rb +13 -0
- data/lib/foxtail/syntax/parser/ast/function_reference.rb +23 -0
- data/lib/foxtail/syntax/parser/ast/group_comment.rb +13 -0
- data/lib/foxtail/syntax/parser/ast/identifier.rb +19 -0
- data/lib/foxtail/syntax/parser/ast/junk.rb +23 -0
- data/lib/foxtail/syntax/parser/ast/message.rb +28 -0
- data/lib/foxtail/syntax/parser/ast/message_reference.rb +23 -0
- data/lib/foxtail/syntax/parser/ast/named_argument.rb +23 -0
- data/lib/foxtail/syntax/parser/ast/number_literal.rb +24 -0
- data/lib/foxtail/syntax/parser/ast/pattern.rb +22 -0
- data/lib/foxtail/syntax/parser/ast/placeable.rb +21 -0
- data/lib/foxtail/syntax/parser/ast/resource.rb +55 -0
- data/lib/foxtail/syntax/parser/ast/resource_comment.rb +13 -0
- data/lib/foxtail/syntax/parser/ast/select_expression.rb +23 -0
- data/lib/foxtail/syntax/parser/ast/span.rb +22 -0
- data/lib/foxtail/syntax/parser/ast/string_literal.rb +45 -0
- data/lib/foxtail/syntax/parser/ast/syntax_node.rb +22 -0
- data/lib/foxtail/syntax/parser/ast/term.rb +28 -0
- data/lib/foxtail/syntax/parser/ast/term_reference.rb +25 -0
- data/lib/foxtail/syntax/parser/ast/text_element.rb +19 -0
- data/lib/foxtail/syntax/parser/ast/variable_reference.rb +21 -0
- data/lib/foxtail/syntax/parser/ast/variant.rb +25 -0
- data/lib/foxtail/syntax/parser/ast.rb +12 -0
- data/lib/foxtail/syntax/parser/parse_error.rb +94 -0
- data/lib/foxtail/syntax/parser/stream.rb +338 -0
- data/lib/foxtail/syntax/parser.rb +797 -0
- data/lib/foxtail/syntax/serializer.rb +242 -0
- data/lib/foxtail/syntax/visitor.rb +61 -0
- data/lib/foxtail/syntax.rb +12 -0
- data/lib/foxtail/tools/error.rb +8 -0
- data/lib/foxtail/tools/version.rb +9 -0
- data/lib/foxtail-tools.rb +22 -0
- metadata +141 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foxtail
|
|
4
|
+
module Syntax
|
|
5
|
+
class Parser
|
|
6
|
+
# Ruby equivalent of fluent.js FluentParserStream
|
|
7
|
+
# Handles character stream processing with CRLF normalization and parsing utilities
|
|
8
|
+
class Stream
|
|
9
|
+
# Constants
|
|
10
|
+
EOL = "\n"
|
|
11
|
+
public_constant :EOL
|
|
12
|
+
|
|
13
|
+
# End of file marker
|
|
14
|
+
EOF = nil
|
|
15
|
+
public_constant :EOF
|
|
16
|
+
|
|
17
|
+
# Characters that have special meaning at the start of a line in FTL
|
|
18
|
+
SPECIAL_LINE_START_CHARS = ["}", ".", "[", "*"].freeze
|
|
19
|
+
public_constant :SPECIAL_LINE_START_CHARS
|
|
20
|
+
|
|
21
|
+
attr_reader :string
|
|
22
|
+
attr_reader :index
|
|
23
|
+
attr_reader :peek_offset
|
|
24
|
+
|
|
25
|
+
def initialize(string)
|
|
26
|
+
@string = string
|
|
27
|
+
@index = 0
|
|
28
|
+
@peek_offset = 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Base stream methods (from ParserStream)
|
|
32
|
+
|
|
33
|
+
def char_at(offset)
|
|
34
|
+
# When the cursor is at CRLF, return LF but don't move the cursor.
|
|
35
|
+
# The cursor still points to the EOL position, which in this case is the
|
|
36
|
+
# beginning of the compound CRLF sequence. This ensures slices of
|
|
37
|
+
# [inclusive, exclusive) continue to work properly.
|
|
38
|
+
if @string[offset] == "\r" && @string[offset + 1] == "\n"
|
|
39
|
+
return "\n"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@string[offset]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @api private
|
|
46
|
+
def current_char = char_at(@index)
|
|
47
|
+
|
|
48
|
+
# @api private
|
|
49
|
+
def current_peek = char_at(@index + @peek_offset)
|
|
50
|
+
|
|
51
|
+
# @api private
|
|
52
|
+
def next
|
|
53
|
+
@peek_offset = 0
|
|
54
|
+
# Skip over the CRLF as if it was a single character.
|
|
55
|
+
if @string[@index] == "\r" && @string[@index + 1] == "\n"
|
|
56
|
+
@index += 1
|
|
57
|
+
end
|
|
58
|
+
@index += 1
|
|
59
|
+
@string[@index]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @api private
|
|
63
|
+
def peek
|
|
64
|
+
# Skip over the CRLF as if it was a single character.
|
|
65
|
+
if @string[@index + @peek_offset] == "\r" && @string[@index + @peek_offset + 1] == "\n"
|
|
66
|
+
@peek_offset += 1
|
|
67
|
+
end
|
|
68
|
+
@peek_offset += 1
|
|
69
|
+
@string[@index + @peek_offset]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @api private
|
|
73
|
+
def reset_peek(offset=0) = @peek_offset = offset
|
|
74
|
+
|
|
75
|
+
# @api private
|
|
76
|
+
def skip_to_peek
|
|
77
|
+
@index += @peek_offset
|
|
78
|
+
@peek_offset = 0
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# FluentParserStream methods
|
|
82
|
+
|
|
83
|
+
# @api private
|
|
84
|
+
def peek_blank_inline
|
|
85
|
+
start = @index + @peek_offset
|
|
86
|
+
peek while current_peek == " "
|
|
87
|
+
@string.slice(start, @index + @peek_offset - start)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @api private
|
|
91
|
+
def skip_blank_inline
|
|
92
|
+
blank = peek_blank_inline
|
|
93
|
+
skip_to_peek
|
|
94
|
+
blank
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @api private
|
|
98
|
+
def peek_blank_block
|
|
99
|
+
blank = ""
|
|
100
|
+
loop do
|
|
101
|
+
line_start = @peek_offset
|
|
102
|
+
peek_blank_inline
|
|
103
|
+
if current_peek == EOL
|
|
104
|
+
blank += EOL
|
|
105
|
+
peek
|
|
106
|
+
next
|
|
107
|
+
end
|
|
108
|
+
if current_peek == EOF
|
|
109
|
+
# Treat the blank line at EOF as a blank block.
|
|
110
|
+
return blank
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Any other char; reset to column 1 on this line.
|
|
114
|
+
reset_peek(line_start)
|
|
115
|
+
return blank
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @api private
|
|
120
|
+
def skip_blank_block
|
|
121
|
+
blank = peek_blank_block
|
|
122
|
+
skip_to_peek
|
|
123
|
+
blank
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @api private
|
|
127
|
+
def peek_blank = (peek while current_peek == " " || current_peek == EOL)
|
|
128
|
+
|
|
129
|
+
# @api private
|
|
130
|
+
def skip_blank
|
|
131
|
+
peek_blank
|
|
132
|
+
skip_to_peek
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def expect_char(ch)
|
|
136
|
+
if current_char == ch
|
|
137
|
+
self.next
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
raise ParseError.new("E0003", ch)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def expect_line_end
|
|
145
|
+
if current_char == EOF
|
|
146
|
+
# EOF is a valid line end in Fluent.
|
|
147
|
+
return
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if current_char == EOL
|
|
151
|
+
self.next
|
|
152
|
+
return
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Unicode Character 'SYMBOL FOR NEWLINE' (U+2424)
|
|
156
|
+
raise ParseError.new("E0003", "\u2424")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @api private
|
|
160
|
+
def take_char
|
|
161
|
+
ch = current_char
|
|
162
|
+
if ch == EOF
|
|
163
|
+
return EOF
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if yield(ch)
|
|
167
|
+
self.next
|
|
168
|
+
return ch
|
|
169
|
+
end
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def char_id_start?(ch)
|
|
174
|
+
return false if ch == EOF
|
|
175
|
+
|
|
176
|
+
cc = ch.ord
|
|
177
|
+
cc.between?(97, 122) || # a-z
|
|
178
|
+
cc.between?(65, 90) # A-Z
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def identifier_start? = char_id_start?(current_peek)
|
|
182
|
+
|
|
183
|
+
def number_start?
|
|
184
|
+
ch = current_char == "-" ? peek : current_char
|
|
185
|
+
|
|
186
|
+
if ch == EOF
|
|
187
|
+
reset_peek
|
|
188
|
+
return false
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
cc = ch.ord
|
|
192
|
+
is_digit = cc.between?(48, 57) # 0-9
|
|
193
|
+
reset_peek
|
|
194
|
+
is_digit
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def char_pattern_continuation?(ch)
|
|
198
|
+
return false if ch == EOF
|
|
199
|
+
|
|
200
|
+
!SPECIAL_LINE_START_CHARS.include?(ch)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def value_start?
|
|
204
|
+
# Inline Patterns may start with any char.
|
|
205
|
+
ch = current_peek
|
|
206
|
+
ch != EOL && ch != EOF
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def value_continuation?
|
|
210
|
+
column1 = @peek_offset
|
|
211
|
+
peek_blank_inline
|
|
212
|
+
|
|
213
|
+
if current_peek == "{"
|
|
214
|
+
reset_peek(column1)
|
|
215
|
+
return true
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
if @peek_offset - column1 == 0
|
|
219
|
+
return false
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
if char_pattern_continuation?(current_peek)
|
|
223
|
+
reset_peek(column1)
|
|
224
|
+
return true
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
false
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# @param level - -1: any, 0: comment, 1: group comment, 2: resource comment
|
|
231
|
+
def next_line_comment?(level=-1)
|
|
232
|
+
return false if current_char != EOL
|
|
233
|
+
|
|
234
|
+
i = 0
|
|
235
|
+
|
|
236
|
+
while i <= level || (level == -1 && i < 3)
|
|
237
|
+
if peek != "#"
|
|
238
|
+
if i <= level && level != -1
|
|
239
|
+
reset_peek
|
|
240
|
+
return false
|
|
241
|
+
end
|
|
242
|
+
break
|
|
243
|
+
end
|
|
244
|
+
i += 1
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# The first char after #, ## or ###.
|
|
248
|
+
ch = peek
|
|
249
|
+
if ch == " " || ch == EOL
|
|
250
|
+
reset_peek
|
|
251
|
+
return true
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
reset_peek
|
|
255
|
+
false
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def variant_start?
|
|
259
|
+
current_peek_offset = @peek_offset
|
|
260
|
+
if current_peek == "*"
|
|
261
|
+
peek
|
|
262
|
+
end
|
|
263
|
+
if current_peek == "["
|
|
264
|
+
reset_peek(current_peek_offset)
|
|
265
|
+
return true
|
|
266
|
+
end
|
|
267
|
+
reset_peek(current_peek_offset)
|
|
268
|
+
false
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def attribute_start? = current_peek == "."
|
|
272
|
+
|
|
273
|
+
# @api private
|
|
274
|
+
def skip_to_next_entry_start(junk_start)
|
|
275
|
+
last_newline = @string.rindex(EOL, @index)
|
|
276
|
+
if last_newline && junk_start < last_newline
|
|
277
|
+
# Last seen newline is _after_ the junk start. It's safe to rewind
|
|
278
|
+
# without the risk of resuming at the same broken entry.
|
|
279
|
+
@index = last_newline
|
|
280
|
+
end
|
|
281
|
+
while current_char
|
|
282
|
+
# We're only interested in beginnings of line.
|
|
283
|
+
unless current_char == EOL
|
|
284
|
+
self.next
|
|
285
|
+
next
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Break if the first char in this line looks like an entry start.
|
|
289
|
+
first = self.next
|
|
290
|
+
if char_id_start?(first) || first == "-" || first == "#"
|
|
291
|
+
break
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def take_id_start
|
|
297
|
+
if char_id_start?(current_char)
|
|
298
|
+
ret = current_char
|
|
299
|
+
self.next
|
|
300
|
+
return ret
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
raise ParseError.new("E0004", "a-zA-Z")
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# @api private
|
|
307
|
+
def take_id_char
|
|
308
|
+
take_char do |ch|
|
|
309
|
+
cc = ch.ord
|
|
310
|
+
cc.between?(97, 122) || # a-z
|
|
311
|
+
cc.between?(65, 90) || # A-Z
|
|
312
|
+
cc.between?(48, 57) || # 0-9
|
|
313
|
+
cc == 95 || # _
|
|
314
|
+
cc == 45 # -
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# @api private
|
|
319
|
+
def take_digit
|
|
320
|
+
take_char do |ch|
|
|
321
|
+
cc = ch.ord
|
|
322
|
+
cc.between?(48, 57) # 0-9
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# @api private
|
|
327
|
+
def take_hex_digit
|
|
328
|
+
take_char do |ch|
|
|
329
|
+
cc = ch.ord
|
|
330
|
+
cc.between?(48, 57) || # 0-9
|
|
331
|
+
cc.between?(65, 70) || # A-F
|
|
332
|
+
cc.between?(97, 102) # a-f
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|