sdl4r 0.9.1

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.
@@ -0,0 +1,129 @@
1
+ # Simple Declarative Language (SDL) for Ruby
2
+ # Copyright 2005 Ikayzo, inc.
3
+ #
4
+ # This program is free software. You can distribute or modify it under the
5
+ # terms of the GNU Lesser General Public License version 2.1 as published by
6
+ # the Free Software Foundation.
7
+ #
8
+ # This program is distributed AS IS and WITHOUT WARRANTY. OF ANY KIND,
9
+ # INCLUDING MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
10
+ # See the GNU Lesser General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU Lesser General Public License
13
+ # along with this program; if not, contact the Free Software Foundation, Inc.,
14
+ # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15
+
16
+
17
+ module SDL4R
18
+
19
+ require File.dirname(__FILE__) + '/parser'
20
+
21
+ # An SDL token.
22
+ #
23
+ # @author Daniel Leuck, Philippe Vosges
24
+ #
25
+ class Token
26
+
27
+ def initialize(text, line = -1, position = -1)
28
+ @text = text
29
+ @line = line
30
+ @pos = position
31
+ @size = text.length
32
+
33
+ begin
34
+ @type = nil
35
+ @object = nil
36
+
37
+ if text =~ /^["`]/
38
+ @type = :STRING
39
+ @object = Parser.parse_string(text)
40
+
41
+ elsif text =~ /^'/
42
+ @type = :CHARACTER
43
+ @object = text[1...-1]
44
+
45
+ elsif text == "null"
46
+ @type = :NULL
47
+ @object = nil
48
+
49
+ elsif text =~ /^true$|^on$/
50
+ @type = :BOOLEAN
51
+ @object = true
52
+
53
+ elsif text =~ /^false$|^off$/
54
+ @type = :BOOLEAN
55
+ @object = false
56
+
57
+ elsif text =~ /^\[/
58
+ @type=:BINARY
59
+ @object = Parser.parse_binary(text)
60
+
61
+ elsif text =~ /^\d+\/\d+\/\d+$/
62
+ @type = :DATE;
63
+ @object = Parser.parse_date_time(text)
64
+
65
+ elsif text =~ /^-?\d+d?:\d+/
66
+ @type = :TIME
67
+ @object = parse_time_span_with_zone(text)
68
+
69
+ elsif text =~ /^[\d\-\.]/
70
+ @type = :NUMBER
71
+ @object = Parser.parse_number(text)
72
+
73
+ else
74
+ case text[0]
75
+ when ?{
76
+ @type = :START_BLOCK
77
+ when ?}
78
+ @type = :END_BLOCK
79
+ when ?=
80
+ @type = :EQUALS
81
+ when ?:
82
+ @type = :COLON
83
+ when ?;
84
+ @type = :SEMICOLON
85
+ end
86
+ end
87
+
88
+ rescue ArgumentError
89
+ raise SdlParseError.new($!.message, @line, @pos)
90
+ end
91
+
92
+ @type = :IDENTIFIER if @type.nil? # if all hope is lost, it's an identifier
93
+
94
+ @punctuation =
95
+ @type == :COLON || @type == :SEMICOLON || @type == :EQUALS ||
96
+ @type == :START_BLOCK || @type == :END_BLOCK
97
+ @literal = @type != :IDENTIFIER && !@punctuation
98
+ end
99
+
100
+ attr_reader :text, :type, :line, :position
101
+
102
+ def literal?
103
+ @literal
104
+ end
105
+
106
+ # Returns the Ruby object corresponding to this literal (or nil if it is
107
+ # not a literal).
108
+ def object_for_literal
109
+ return @object
110
+ end
111
+
112
+ def to_s
113
+ @type.to_s + " " + @text + " pos:" + @pos.to_s
114
+ end
115
+
116
+ # This special parse method is used only by the Token class for
117
+ # tokens which are ambiguously either a TimeSpan or the time component
118
+ # of a date/time type
119
+ def parse_time_span_with_zone(literal)
120
+ raise ArgumentError("time span or date literal is nil") if literal.nil?
121
+
122
+ days, hours, minutes, seconds, time_zone_offset =
123
+ Parser.parse_time_span_and_time_zone(literal, true, true)
124
+
125
+ return Parser::TimeSpanWithZone.new(days, hours, minutes, seconds, time_zone_offset)
126
+ end
127
+ end
128
+
129
+ end
@@ -0,0 +1,501 @@
1
+ # Simple Declarative Language (SDL) for Ruby
2
+ # Copyright 2005 Ikayzo, inc.
3
+ #
4
+ # This program is free software. You can distribute or modify it under the
5
+ # terms of the GNU Lesser General Public License version 2.1 as published by
6
+ # the Free Software Foundation.
7
+ #
8
+ # This program is distributed AS IS and WITHOUT WARRANTY. OF ANY KIND,
9
+ # INCLUDING MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
10
+ # See the GNU Lesser General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU Lesser General Public License
13
+ # along with this program; if not, contact the Free Software Foundation, Inc.,
14
+ # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15
+
16
+
17
+ module SDL4R
18
+
19
+ require File.dirname(__FILE__) + '/reader'
20
+ require File.dirname(__FILE__) + '/token'
21
+
22
+ # Tokenizer of the SDL parser
23
+ class Tokenizer
24
+
25
+ TOKEN_TYPES = [
26
+ :IDENTIFIER,
27
+
28
+ # punctuation
29
+ :COLON, :SEMICOLON, :EQUALS, :START_BLOCK, :END_BLOCK,
30
+
31
+ # literals
32
+ :STRING, :CHARACTER, :BOOLEAN, :NUMBER, :DATE, :TIME, :BINARY, :NULL ]
33
+
34
+
35
+ # Creates an SDL tokenizer on the specified +IO+.
36
+ def initialize(io)
37
+ raise ArgumentError, "io == nil" if io.nil?
38
+
39
+ @reader = Reader.new(io)
40
+ @token_start = 0
41
+ @startEscapedQuoteLine = false
42
+ @tokens = nil
43
+ @tokenText = nil
44
+ end
45
+
46
+ # Closes this Tokenizer and its underlying +Reader+.
47
+ def close
48
+ @reader.close
49
+ end
50
+
51
+ # Close the reader and throw a SdlParseError.
52
+ def parse_error(description, line_no = nil, position = nil)
53
+ begin
54
+ @reader.close()
55
+ rescue IOError
56
+ # no recourse
57
+ end
58
+
59
+ line_no = @reader.line_no if line_no.nil?
60
+ position = @reader.pos if position.nil?
61
+
62
+ # We add one because editors typically start with line 1 and position 1
63
+ # rather than 0...
64
+ raise SdlParseError.new(description, line_no + 1, position + 1, @reader.line)
65
+ end
66
+
67
+ # Close the reader and throw a SdlParseError using the format
68
+ # Was expecting X but got Y.
69
+ #
70
+ def expecting_but_got(expecting, got, line, position)
71
+ parse_error("Was expecting #{expecting} but got #{got}", line, position)
72
+ end
73
+
74
+ # Returns the next line as tokens or nil if the end of the stream has been reached.
75
+ # This method handles line continuations both within and outside String literals.
76
+ # The line of tokens is assigned to @tokens.
77
+ #
78
+ # Returns a logical line as a list of Tokens.
79
+ #
80
+ def read_line_tokens
81
+ begin
82
+ read_line_tokens_even_if_empty()
83
+ end until @tokens.nil? or !@tokens.empty?
84
+ return @tokens
85
+ end
86
+
87
+ def line_no
88
+ @reader.line_no
89
+ end
90
+
91
+ def pos
92
+ @reader.pos
93
+ end
94
+
95
+ def line
96
+ @reader.line
97
+ end
98
+
99
+ private
100
+
101
+ # Returns the next line as tokens or nil if the end of the stream has been reached.
102
+ # This method handles line continuations both within and outside String literals.
103
+ # The line of tokens is assigned to @tokens.
104
+ #
105
+ # Returns a logical line as a list of Tokens.
106
+ # Returns an empty array if the line was empty.
107
+ #
108
+ def read_line_tokens_even_if_empty
109
+ @tokens = nil
110
+ @tokenText = nil
111
+ @token_start = nil
112
+
113
+ @reader.read_line() if @reader.end_of_line?
114
+ return @tokens unless @reader.line
115
+
116
+ @tokens = []
117
+ @token_start = @reader.pos
118
+
119
+ while not @reader.end_of_line?
120
+ if @tokenText
121
+ @tokens << Token.new(@tokenText, @reader.line_no, @token_start)
122
+ @tokenText = nil
123
+ end
124
+
125
+ c = @reader.current_char
126
+ next_c = @reader.get_line_char(@reader.pos + 1)
127
+ case c
128
+ when "\""
129
+ # handle "" style strings including line continuations
130
+ handle_double_quote_string()
131
+
132
+ when "'"
133
+ handle_character_literal()
134
+
135
+ when "{", "}", "=", ":", ";"
136
+ # handle punctuation
137
+ punctuation_token = Token.new(c, @reader.line_no, @reader.pos)
138
+ @tokenText = nil
139
+
140
+ if punctuation_token.type == :SEMICOLON
141
+ @reader.skip_char()
142
+ break
143
+ else
144
+ @tokens << punctuation_token
145
+ end
146
+
147
+ when "#"
148
+ # skip : hash comment at end of line
149
+ @reader.skip_line()
150
+
151
+ when "/"
152
+ # handle // and /**/ style comments
153
+ if next_c == "/"
154
+ # skip : // comment
155
+ @reader.skip_line()
156
+ else
157
+ handle_slash_comment()
158
+ end
159
+
160
+ when "`"
161
+ # handle multiline `` style strings
162
+ handle_back_quote_string()
163
+
164
+ when "["
165
+ # handle binary literals
166
+ handle_binary_literal()
167
+
168
+ when "\s", "\t"
169
+ @reader.skip_whitespaces()
170
+
171
+ when "\\"
172
+ # line continuations (outside a string literal)
173
+ handle_line_continuation();
174
+
175
+ when /^[0-9\-\.]$/
176
+ if c == "-" and next_c == "-"
177
+ # -- comments : ignore
178
+ @reader.skip_line()
179
+ else
180
+ # handle numbers, dates, and time spans
181
+ handle_number_date_or_time_span()
182
+ end
183
+
184
+ when /^[a-zA-Z\$_]$/
185
+ # FIXME Here, the Java code specifies isJavaIdentifierStart() but
186
+ # this is not easily implemented (at least as of Ruby 1.8).
187
+ # So, we implement a subset of these characters.
188
+ handle_identifier()
189
+
190
+ when "\n", "\r"
191
+ # end of line
192
+ @reader.skip_line()
193
+
194
+ else
195
+ parse_error("Unexpected character '#{c}'")
196
+ end
197
+
198
+ @reader.skip_char()
199
+ end
200
+
201
+ if @tokenText
202
+ @tokens << Token.new(@tokenText, @reader.line_no, @token_start)
203
+ end
204
+
205
+ return @tokens
206
+ end
207
+
208
+ # Adds the current escaped character (represented by ((|c|))) to @tokenText.
209
+ # This method assumes the previous char was a backslash.
210
+ #
211
+ def add_escaped_char_in_string(c)
212
+ case c
213
+ when "\\", "\""
214
+ @tokenText << c
215
+ when "n"
216
+ @tokenText << ?\n
217
+ when "r"
218
+ @tokenText << ?\r
219
+ when "t"
220
+ @tokenText << ?\t
221
+ else
222
+ parse_error("Illegal escape character in string literal: '#{c.chr}'.")
223
+ end
224
+ end
225
+
226
+ def handle_double_quote_string
227
+ escaped = false
228
+ @startEscapedQuoteLine = false
229
+
230
+ @tokenText = "\""
231
+ @reader.skip_char()
232
+
233
+ while not @reader.end_of_line?
234
+ c = @reader.current_char
235
+
236
+ if "\s\t".include?(c) and @startEscapedQuoteLine
237
+ # we continue
238
+ else
239
+ @startEscapedQuoteLine = false;
240
+
241
+ if escaped
242
+ add_escaped_char_in_string(c)
243
+ escaped = false
244
+
245
+ elsif c == "\\"
246
+ # check for String broken across lines
247
+ if @reader.rest_of_line =~ /^\\\s*$/
248
+ handle_escaped_double_quoted_string()
249
+ next # as we are at the beginning of a new line
250
+ else
251
+ escaped = true;
252
+ end
253
+
254
+ else
255
+ @tokenText << c
256
+ if c == "\""
257
+ # end of double-quoted string detected
258
+ @tokens << Token.new(@tokenText, @reader.line_no, @token_start)
259
+ @tokenText = nil
260
+ return
261
+ end
262
+ end
263
+ end
264
+
265
+ @reader.skip_char()
266
+ end
267
+
268
+ # detection of ill-terminated literals
269
+ if @tokenText =~ /^".*[^"]$/
270
+ parse_error(
271
+ "String literal \"#{@tokenText}\" not terminated by end quote.", @reader.line_no, @reader.line_length);
272
+ elsif @tokenText == "\""
273
+ parse_error("Orphan quote (unterminated string)", @reader.line_no, @reader.line_length);
274
+ end
275
+ end
276
+
277
+ def handle_escaped_double_quoted_string
278
+ # '\' can be followed by whitespaces
279
+ if @reader.rest_of_line =~ /^\\\s*$/
280
+ @reader.read_line()
281
+ parse_error("Escape at end of file.") if @reader.end_of_file?
282
+
283
+ @startEscapedQuoteLine = true
284
+
285
+ else
286
+ parse_error(
287
+ "Malformed string literal - escape followed by whitespace " +
288
+ "followed by non-whitespace.")
289
+ end
290
+ end
291
+
292
+ def handle_character_literal
293
+ if not @reader.more_chars_in_line?
294
+ parse_error("Got ' at end of line")
295
+ end
296
+
297
+ c2 = @reader.read_char()
298
+
299
+ if c2 == "\\"
300
+ if @reader.end_of_line?
301
+ parse_error("Got '\\ at end of line")
302
+ end
303
+
304
+ c3 = @reader.read_char()
305
+
306
+ if not @reader.more_chars_in_line?
307
+ parse_error("Got '\\#{c3} at end of line")
308
+ end
309
+
310
+ case c3
311
+ when "\\"
312
+ @tokens << Token.new("'\\'", @reader.line_no, @reader.pos)
313
+ when "'"
314
+ @tokens << Token.new("'''", @reader.line_no, @reader.pos)
315
+ when "n"
316
+ @tokens << Token.new("'\n'", @reader.line_no, @reader.pos)
317
+ when "r"
318
+ @tokens << Token.new("'\r'", @reader.line_no, @reader.pos)
319
+ when "t"
320
+ @tokens << Token.new("'\t'", @reader.line_no, @reader.pos)
321
+ else
322
+ parse_error("Illegal escape character #{@reader.current_char}")
323
+ end
324
+
325
+ @reader.skip_char()
326
+ if @reader.current_char != "'"
327
+ expecting_but_got("single quote (')", "\"#{@reader.current_char}\"")
328
+ end
329
+ else
330
+ @tokens << Token.new("'#{c2}'", @reader.line_no, @reader.pos)
331
+ if not @reader.more_chars_in_line?
332
+ parse_error("Got '#{c2} at end of line")
333
+ end
334
+ @reader.skip_char()
335
+ if @reader.current_char != "'"
336
+ expecting_but_got(
337
+ "quote (')", "\"#{@reader.current_char}\"", @reader.line_no, @reader.pos)
338
+ end
339
+ end
340
+ end
341
+
342
+ def handle_slash_comment
343
+ if not @reader.more_chars_in_line?
344
+ parse_error("Got slash (/) at end of line.")
345
+ end
346
+
347
+ if @reader.get_line_char(@reader.pos + 1) == "*"
348
+ end_index = @reader.find_next_in_line("*/")
349
+ if end_index
350
+ # handle comment on same line
351
+ @reader.skip_to(end_index + 1)
352
+ else
353
+ # handle multiline comments
354
+ loop do
355
+ @reader.read_raw_line()
356
+ if @reader.end_of_file?
357
+ parse_error("/* comment not terminated.", @reader.line_no, -2)
358
+ end
359
+
360
+ end_index = @reader.find_next_in_line("*/", 0)
361
+
362
+ if end_index
363
+ @reader.skip_to(end_index + 1)
364
+ break
365
+ end
366
+ end
367
+ end
368
+ elsif @reader.get_line_char(@reader.pos + 1) == "/"
369
+ parse_error("Got slash (/) in unexpected location.")
370
+ end
371
+ end
372
+
373
+ def handle_back_quote_string
374
+ end_index = @reader.find_next_in_line("`")
375
+
376
+ if end_index
377
+ # handle end quote on same line
378
+ @tokens << Token.new(@reader.substring(@reader.pos, end_index), @reader.line_no, @reader.pos)
379
+ @tokenText = nil
380
+ @reader.skip_to(end_index)
381
+
382
+ else
383
+ @tokenText = @reader.rest_of_line
384
+ @token_start = @reader.pos
385
+ # handle multiline quotes
386
+ loop do
387
+ @reader.read_raw_line()
388
+ if @reader.end_of_file?
389
+ parse_error("` quote not terminated.", @reader.line_no, -2)
390
+ end
391
+
392
+ end_index = @reader.find_next_in_line("`", 0)
393
+ if end_index
394
+ @tokenText << @reader.substring(0, end_index)
395
+ @reader.skip_to(end_index)
396
+ break
397
+ else
398
+ @tokenText << @reader.line
399
+ end
400
+ end
401
+
402
+ @tokens << Token.new(@tokenText, @reader.line_no, @token_start)
403
+ @tokenText = nil
404
+ end
405
+ end
406
+
407
+ def handle_binary_literal
408
+ end_index = @reader.find_next_in_line("]")
409
+
410
+ if end_index
411
+ # handle end quote on same line
412
+ @tokens << Token.new(@reader.substring(@reader.pos, end_index), @reader.line_no, @reader.pos)
413
+ @tokenText = nil
414
+ @reader.skip_to(end_index)
415
+ else
416
+ @tokenText = @reader.substring(@reader.pos)
417
+ @token_start = @reader.pos
418
+ # handle multiline quotes
419
+ loop do
420
+ @reader.read_raw_line()
421
+ if @reader.end_of_file?
422
+ parse_error("[base64] binary literal not terminated.", @reader.line_no, -2)
423
+ end
424
+
425
+ end_index = @reader.find_next_in_line("]", 0)
426
+ if end_index
427
+ @tokenText << @reader.substring(0, end_index)
428
+ @reader.skip_to(end_index)
429
+ break
430
+ else
431
+ @tokenText << @reader.line
432
+ end
433
+ end
434
+
435
+ @tokens << Token.new(@tokenText, @reader.line_no, @token_start)
436
+ @tokenText = nil
437
+ end
438
+ end
439
+
440
+ # handle a line continuation (not inside a string)
441
+ def handle_line_continuation
442
+ # backslash line continuation outside of a String literal
443
+ # can only occur at the end of a line
444
+ if not @reader.rest_of_line =~ /^\\\s*$/
445
+ parse_error("Line continuation (\\) before end of line")
446
+ else
447
+ @line = @reader.read_line()
448
+ if @line.nil?
449
+ parse_error("Line continuation at end of file.", @reader.line_no, @reader.pos)
450
+ end
451
+ end
452
+ end
453
+
454
+ def handle_number_date_or_time_span
455
+ @token_start = @reader.pos
456
+ @tokenText = ""
457
+
458
+ while not @reader.end_of_line?
459
+ c = @reader.current_char
460
+
461
+ if c =~ /[\w\.\-+:]/
462
+ @tokenText << c
463
+ elsif c == "/" and not @reader.get_line_char(@reader.pos + 1) == "*"
464
+ @tokenText << c
465
+ else
466
+ @reader.previous_char()
467
+ break
468
+ end
469
+
470
+ @reader.skip_char()
471
+ end
472
+
473
+ @tokens << Token.new(@tokenText, @reader.line_no, @token_start)
474
+ @tokenText = nil
475
+ end
476
+
477
+ def handle_identifier
478
+ @token_start = @reader.pos;
479
+ @tokenText = ""
480
+
481
+ while not @reader.end_of_line?
482
+ c = @reader.current_char
483
+
484
+ # FIXME here we are stricter than the Java version because there is no
485
+ # easy way to implement Character.isJavaIdentifierPart() in Ruby :)
486
+ if c =~ /[\w_$-]/
487
+ @tokenText << c
488
+ else
489
+ @reader.previous_char()
490
+ break
491
+ end
492
+
493
+ @reader.skip_char()
494
+ end
495
+
496
+ @tokens << Token.new(@tokenText, @reader.line_no, @token_start)
497
+ @tokenText = nil
498
+ end
499
+
500
+ end
501
+ end