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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +19 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +66 -0
  5. data/exe/foxtail +12 -0
  6. data/lib/foxtail/cli/commands/check.rb +60 -0
  7. data/lib/foxtail/cli/commands/dump.rb +43 -0
  8. data/lib/foxtail/cli/commands/ids.rb +73 -0
  9. data/lib/foxtail/cli/commands/tidy.rb +107 -0
  10. data/lib/foxtail/cli.rb +59 -0
  11. data/lib/foxtail/syntax/error.rb +8 -0
  12. data/lib/foxtail/syntax/parser/ast/annotation.rb +23 -0
  13. data/lib/foxtail/syntax/parser/ast/attribute.rb +23 -0
  14. data/lib/foxtail/syntax/parser/ast/base_comment.rb +19 -0
  15. data/lib/foxtail/syntax/parser/ast/base_literal.rb +24 -0
  16. data/lib/foxtail/syntax/parser/ast/base_node.rb +89 -0
  17. data/lib/foxtail/syntax/parser/ast/call_arguments.rb +23 -0
  18. data/lib/foxtail/syntax/parser/ast/comment.rb +13 -0
  19. data/lib/foxtail/syntax/parser/ast/function_reference.rb +23 -0
  20. data/lib/foxtail/syntax/parser/ast/group_comment.rb +13 -0
  21. data/lib/foxtail/syntax/parser/ast/identifier.rb +19 -0
  22. data/lib/foxtail/syntax/parser/ast/junk.rb +23 -0
  23. data/lib/foxtail/syntax/parser/ast/message.rb +28 -0
  24. data/lib/foxtail/syntax/parser/ast/message_reference.rb +23 -0
  25. data/lib/foxtail/syntax/parser/ast/named_argument.rb +23 -0
  26. data/lib/foxtail/syntax/parser/ast/number_literal.rb +24 -0
  27. data/lib/foxtail/syntax/parser/ast/pattern.rb +22 -0
  28. data/lib/foxtail/syntax/parser/ast/placeable.rb +21 -0
  29. data/lib/foxtail/syntax/parser/ast/resource.rb +55 -0
  30. data/lib/foxtail/syntax/parser/ast/resource_comment.rb +13 -0
  31. data/lib/foxtail/syntax/parser/ast/select_expression.rb +23 -0
  32. data/lib/foxtail/syntax/parser/ast/span.rb +22 -0
  33. data/lib/foxtail/syntax/parser/ast/string_literal.rb +45 -0
  34. data/lib/foxtail/syntax/parser/ast/syntax_node.rb +22 -0
  35. data/lib/foxtail/syntax/parser/ast/term.rb +28 -0
  36. data/lib/foxtail/syntax/parser/ast/term_reference.rb +25 -0
  37. data/lib/foxtail/syntax/parser/ast/text_element.rb +19 -0
  38. data/lib/foxtail/syntax/parser/ast/variable_reference.rb +21 -0
  39. data/lib/foxtail/syntax/parser/ast/variant.rb +25 -0
  40. data/lib/foxtail/syntax/parser/ast.rb +12 -0
  41. data/lib/foxtail/syntax/parser/parse_error.rb +94 -0
  42. data/lib/foxtail/syntax/parser/stream.rb +338 -0
  43. data/lib/foxtail/syntax/parser.rb +797 -0
  44. data/lib/foxtail/syntax/serializer.rb +242 -0
  45. data/lib/foxtail/syntax/visitor.rb +61 -0
  46. data/lib/foxtail/syntax.rb +12 -0
  47. data/lib/foxtail/tools/error.rb +8 -0
  48. data/lib/foxtail/tools/version.rb +9 -0
  49. data/lib/foxtail-tools.rb +22 -0
  50. 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