sdl4r 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +3 -0
- data/Rakefile +45 -0
- data/TODO.txt +117 -0
- data/lib/scratchpad.rb +49 -0
- data/lib/sdl4r/parser.rb +678 -0
- data/lib/sdl4r/reader.rb +171 -0
- data/lib/sdl4r/sdl.rb +242 -0
- data/lib/sdl4r/sdl_binary.rb +78 -0
- data/lib/sdl4r/sdl_parse_error.rb +44 -0
- data/lib/sdl4r/sdl_time_span.rb +301 -0
- data/lib/sdl4r/tag.rb +949 -0
- data/lib/sdl4r/token.rb +129 -0
- data/lib/sdl4r/tokenizer.rb +501 -0
- data/test/sdl4r/parser_test.rb +295 -0
- data/test/sdl4r/test.rb +541 -0
- data/test/sdl4r/test_basic_types.sdl +138 -0
- data/test/sdl4r/test_structures.sdl +180 -0
- metadata +81 -0
data/lib/sdl4r/reader.rb
ADDED
@@ -0,0 +1,171 @@
|
|
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
|
+
# Gives access to the characters read to the Parser.
|
20
|
+
# This class was designed to gather the handling of the UTF-8 issues in one place and shield the
|
21
|
+
# Parser class from these problems.
|
22
|
+
class Reader
|
23
|
+
|
24
|
+
RUBY_1_8_OR_LESS = require 'jcode'
|
25
|
+
|
26
|
+
|
27
|
+
# +io+ an open IO from which the characters are read.
|
28
|
+
def initialize(io)
|
29
|
+
raise ArgumentError, "io == nil" if io.nil?
|
30
|
+
|
31
|
+
@io = io
|
32
|
+
@line = nil
|
33
|
+
@line_chars = nil
|
34
|
+
@line_no = 0
|
35
|
+
@pos = 0
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :line_no, :pos, :line;
|
39
|
+
|
40
|
+
def line_length
|
41
|
+
return @line_chars.nil? ? 0 : @line_chars.length
|
42
|
+
end
|
43
|
+
|
44
|
+
# Reads next line in stream skipping comment lines and blank lines.
|
45
|
+
#
|
46
|
+
# Returns the next line or nil at the end of the file.
|
47
|
+
def read_line
|
48
|
+
@line_chars = nil
|
49
|
+
|
50
|
+
while @line = read_raw_line()
|
51
|
+
# Skip empty and commented lines
|
52
|
+
break unless @line.empty? or @line =~ /^#/
|
53
|
+
end
|
54
|
+
|
55
|
+
return @line
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the string that goes from the current position of this Reader to the end of the line
|
59
|
+
# or nil if the current position doesn't allow that.
|
60
|
+
def rest_of_line
|
61
|
+
return @line[@pos..-1]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Indicates whether the end of file has been reached.
|
65
|
+
def end_of_file?
|
66
|
+
return @line.nil?
|
67
|
+
end
|
68
|
+
|
69
|
+
# Indicates whether there are more characters in the current line
|
70
|
+
def more_chars_in_line?
|
71
|
+
return @pos < line_length - 1
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns whether the end of the current +line+ as been reached.
|
75
|
+
def end_of_line?
|
76
|
+
return @pos >= line_length
|
77
|
+
end
|
78
|
+
|
79
|
+
# Skips the current line by going just after its end.
|
80
|
+
def skip_line
|
81
|
+
@pos = line_length
|
82
|
+
end
|
83
|
+
|
84
|
+
# Skips the whitespaces that follow the current position.
|
85
|
+
def skip_whitespaces
|
86
|
+
while (@pos + 1) < line_length and
|
87
|
+
(@line[@pos + 1] == ?\s or @line[@pos + 1] == ?\t)
|
88
|
+
@pos += 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns the character at position +pos+ in the current line.
|
93
|
+
# Returns nil if there is no current line or if +pos+ is after the end of the line.
|
94
|
+
def get_line_char(pos)
|
95
|
+
if @line_chars and pos < line_length
|
96
|
+
return @line_chars[pos]
|
97
|
+
else
|
98
|
+
return nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns the character at the current position or nil after end-of-line or end-of -file.
|
103
|
+
def current_char
|
104
|
+
return get_line_char(@pos)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Go to the next character in the stream.
|
108
|
+
def skip_char
|
109
|
+
@pos += 1 if @pos < line_length
|
110
|
+
end
|
111
|
+
|
112
|
+
# Go to the next character and returns it (or nil if end-of-line or -file has been reached).
|
113
|
+
def read_char
|
114
|
+
skip_char()
|
115
|
+
return current_char
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns to the previous char if possible.
|
119
|
+
def previous_char
|
120
|
+
@pos -= 1 if @pos >= -1
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns the next index of the expression (string, regexp, fixnum) in the current line, starting
|
124
|
+
# from after the current position if no position is specified.
|
125
|
+
def find_next_in_line(searched, start_pos = nil)
|
126
|
+
start_pos = @pos + 1 unless start_pos
|
127
|
+
return @line.index(searched, start_pos)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Skips the specified position in the current line.
|
131
|
+
def skip_to(new_pos)
|
132
|
+
@pos = new_pos
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
# Returns a subpart of the current line starting from +from+ and stopping at +to+ (excluded).
|
137
|
+
def substring(from, to = -1)
|
138
|
+
return @line[from..to]
|
139
|
+
end
|
140
|
+
|
141
|
+
# Reads and returns a "raw" line including lines with comments and blank lines.
|
142
|
+
#
|
143
|
+
# Returns the next line or nil if at the end of the file.
|
144
|
+
#
|
145
|
+
# This method changes the value of @line, @lineNo and @pos.
|
146
|
+
def read_raw_line
|
147
|
+
@line = @io.gets()
|
148
|
+
|
149
|
+
# We ensure that only \n is used as an end-of-line by replacing \r and \r\n.
|
150
|
+
if @line
|
151
|
+
if not @line.gsub!(/\r\n/m, "\n")
|
152
|
+
@line.gsub!(/\r/m, "\n")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
@pos = 0;
|
157
|
+
@line_chars = nil
|
158
|
+
if @line
|
159
|
+
@line_no += 1
|
160
|
+
@line_chars = @line.scan(/./m)
|
161
|
+
end
|
162
|
+
return @line
|
163
|
+
end
|
164
|
+
|
165
|
+
# Closes this Reader and its underlying +IO+.
|
166
|
+
def close
|
167
|
+
@io.close
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
data/lib/sdl4r/sdl.rb
ADDED
@@ -0,0 +1,242 @@
|
|
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
|
+
require 'jcode'
|
18
|
+
require 'base64'
|
19
|
+
require 'rational'
|
20
|
+
require 'date'
|
21
|
+
|
22
|
+
# Various SDL related utility methods
|
23
|
+
#
|
24
|
+
module SDL4R
|
25
|
+
|
26
|
+
MAX_INTEGER_32 = 2**31 - 1
|
27
|
+
MIN_INTEGER_32 = -(2**31)
|
28
|
+
|
29
|
+
MAX_INTEGER_64 = 2**63 - 1
|
30
|
+
MIN_INTEGER_64 = -(2**63)
|
31
|
+
|
32
|
+
|
33
|
+
# Returns the milliseonds part of a DateTime (by translating the +sec_fraction+ part) as an
|
34
|
+
# integer.
|
35
|
+
def self.get_date_milliseconds(date)
|
36
|
+
sec_fraction = date.sec_fraction() # in days
|
37
|
+
# 86400000 is the number of milliseconds in a day
|
38
|
+
return (sec_fraction * 86400000).round()
|
39
|
+
end
|
40
|
+
|
41
|
+
BASE64_WRAP_LINE_LENGTH = 72
|
42
|
+
|
43
|
+
# Creates an SDL string representation for a given object and returns it.
|
44
|
+
#
|
45
|
+
# +o+:: the object to format
|
46
|
+
# +add_quotes+:: indicates whether quotes will be added to Strings and characters
|
47
|
+
# (true by default)
|
48
|
+
# +line_prefix+:: the line prefix to use ("" by default)
|
49
|
+
# +indent+:: the indent string to use ("\t" by default)
|
50
|
+
#
|
51
|
+
def self.format(o, add_quotes = true, line_prefix = "", indent = "\t")
|
52
|
+
if o.is_a?(String)
|
53
|
+
if add_quotes
|
54
|
+
o_length = 0
|
55
|
+
o.scan(/./m) { o_length += 1 } # counts the number of chars (as opposed of bytes)
|
56
|
+
if o_length == 1
|
57
|
+
return "'" + escape(o, "'") + "'"
|
58
|
+
else
|
59
|
+
return '"' + escape(o, '"') + '"'
|
60
|
+
end
|
61
|
+
else
|
62
|
+
return escape(o)
|
63
|
+
end
|
64
|
+
|
65
|
+
elsif o.is_a?(Bignum)
|
66
|
+
return o.to_s + "BD"
|
67
|
+
|
68
|
+
elsif o.is_a?(Integer)
|
69
|
+
if MIN_INTEGER_32 <= o and o <= MAX_INTEGER_32
|
70
|
+
return o.to_s
|
71
|
+
elsif MIN_INTEGER_64 <= o and o <= MAX_INTEGER_64
|
72
|
+
return o.to_s + "L"
|
73
|
+
else
|
74
|
+
return o.to_s + "BD"
|
75
|
+
end
|
76
|
+
|
77
|
+
elsif o.is_a?(Float)
|
78
|
+
return o.to_s + "F"
|
79
|
+
|
80
|
+
elsif o.is_a?(Rational)
|
81
|
+
return o.to_f.to_s + "F"
|
82
|
+
|
83
|
+
elsif defined? Flt::DecNum and o.is_a?(Flt::DecNum)
|
84
|
+
return o.to_s + "BD"
|
85
|
+
|
86
|
+
elsif o.nil?
|
87
|
+
return "null"
|
88
|
+
|
89
|
+
elsif o.is_a?(SdlBinary)
|
90
|
+
encoded_o = Base64.encode64(o.bytes)
|
91
|
+
encoded_o.gsub!(/[\r\n]/m, "") # Remove the EOL inserted every 60 chars
|
92
|
+
|
93
|
+
if add_quotes
|
94
|
+
if encoded_o.length > BASE64_WRAP_LINE_LENGTH
|
95
|
+
# FIXME: we should a constant or some parameter instead of hardcoded spaces
|
96
|
+
wrap_lines_in_ascii(encoded_o, BASE64_WRAP_LINE_LENGTH, "#{line_prefix}#{indent}")
|
97
|
+
encoded_o.insert(0, "[#{$/}")
|
98
|
+
encoded_o << "#{$/}#{line_prefix}]"
|
99
|
+
else
|
100
|
+
encoded_o.insert(0, "[")
|
101
|
+
encoded_o << "]"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
return encoded_o
|
106
|
+
|
107
|
+
# Below, we use "#{o.year}" instead of "%Y" because "%Y" always emit 4 chars at least even if
|
108
|
+
# the date is before 1000.
|
109
|
+
elsif o.is_a?(DateTime) || o.is_a?(Time)
|
110
|
+
milliseconds = o.is_a?(DateTime) ? get_date_milliseconds(o) : (o.usec / 10).to_i
|
111
|
+
|
112
|
+
if milliseconds == 0
|
113
|
+
if o.zone
|
114
|
+
return o.strftime("#{o.year}/%m/%d %H:%M:%S%Z")
|
115
|
+
else
|
116
|
+
return o.strftime("#{o.year}/%m/%d %H:%M:%S")
|
117
|
+
end
|
118
|
+
else
|
119
|
+
if o.zone
|
120
|
+
return o.strftime("#{o.year}/%m/%d %H:%M:%S." + milliseconds.to_s.ljust(3, '0') + "%Z")
|
121
|
+
else
|
122
|
+
return o.strftime("#{o.year}/%m/%d %H:%M:%S." + milliseconds.to_s.ljust(3, '0'))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
elsif o.is_a?(Date)
|
127
|
+
return o.strftime("#{o.year}/%m/%d")
|
128
|
+
|
129
|
+
else
|
130
|
+
return o.to_s
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Wraps lines in "s" (by modifying it). This method only supports 1-byte character strings.
|
135
|
+
#
|
136
|
+
def self.wrap_lines_in_ascii(s, line_length, line_prefix = nil)
|
137
|
+
# We could use such code if it supported any value for "line_prefix": unfortunately it is capped
|
138
|
+
# at 64 in the regular expressions.
|
139
|
+
#
|
140
|
+
# return "#{line_prefix}" + encoded_o.scan(/.{1,#{line_prefix}}/).join("#{$/}#{line_prefix}")
|
141
|
+
|
142
|
+
eol_size = "#{$/}".size
|
143
|
+
|
144
|
+
i = 0
|
145
|
+
while i < s.size
|
146
|
+
if i > 0
|
147
|
+
s.insert(i, $/)
|
148
|
+
i += eol_size
|
149
|
+
end
|
150
|
+
|
151
|
+
if line_prefix
|
152
|
+
s.insert(i, line_prefix)
|
153
|
+
i += line_prefix.size
|
154
|
+
end
|
155
|
+
|
156
|
+
i += line_length
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
ESCAPED_QUOTES = {
|
161
|
+
"\"" => "\\\"",
|
162
|
+
"'" => "\\'",
|
163
|
+
"`" => "\\`",
|
164
|
+
}
|
165
|
+
|
166
|
+
ESCAPED_CHARS = {
|
167
|
+
"\\" => "\\\\",
|
168
|
+
"\t" => "\\t",
|
169
|
+
"\r" => "\\r",
|
170
|
+
"\n" => "\\n",
|
171
|
+
}
|
172
|
+
ESCAPED_CHARS.merge!(ESCAPED_QUOTES)
|
173
|
+
|
174
|
+
# Returns an escaped version of +s+ (i.e. where characters which need to be
|
175
|
+
# escaped, are escaped).
|
176
|
+
#
|
177
|
+
def self.escape(s, quote_char = nil)
|
178
|
+
escaped_s = ""
|
179
|
+
|
180
|
+
s.each_char { |c|
|
181
|
+
escaped_char = ESCAPED_CHARS[c]
|
182
|
+
if escaped_char
|
183
|
+
if ESCAPED_QUOTES.has_key?(c)
|
184
|
+
if quote_char && c == quote_char
|
185
|
+
escaped_s << escaped_char
|
186
|
+
else
|
187
|
+
escaped_s << c
|
188
|
+
end
|
189
|
+
else
|
190
|
+
escaped_s << escaped_char
|
191
|
+
end
|
192
|
+
else
|
193
|
+
escaped_s << c
|
194
|
+
end
|
195
|
+
}
|
196
|
+
|
197
|
+
return escaped_s
|
198
|
+
end
|
199
|
+
|
200
|
+
# This method was kept from the Java code but it is not sure if it should have a usefulness yet.
|
201
|
+
#
|
202
|
+
def self.coerce_or_fail(o)
|
203
|
+
return o
|
204
|
+
end
|
205
|
+
|
206
|
+
# Validates an SDL identifier String. SDL Identifiers must start with a
|
207
|
+
# Unicode letter or underscore (_) and contain only unicode letters,
|
208
|
+
# digits, underscores (_), and dashes(-).
|
209
|
+
#
|
210
|
+
# @param identifier The identifier to validate
|
211
|
+
# @throws IllegalArgumentException if the identifier is not legal
|
212
|
+
#
|
213
|
+
# TODO: support UTF-8 identifiers
|
214
|
+
#
|
215
|
+
def self.validate_identifier(identifier)
|
216
|
+
if identifier.nil? or identifier.empty?
|
217
|
+
raise ArgumentError, "SDL identifiers cannot be null or empty."
|
218
|
+
end
|
219
|
+
|
220
|
+
# in Java, was if(!Character.isJavaIdentifierStart(identifier.charAt(0)))
|
221
|
+
unless identifier =~ /^[a-zA-Z_]/
|
222
|
+
raise ArgumentError,
|
223
|
+
"'" + identifier[0..0] +
|
224
|
+
"' is not a legal first character for an SDL identifier. " +
|
225
|
+
"SDL Identifiers must start with a unicode letter or " +
|
226
|
+
"an underscore (_)."
|
227
|
+
end
|
228
|
+
|
229
|
+
unless identifier.length == 1 or identifier =~ /^[a-zA-Z_][a-zA-Z_0-9\-]*$/
|
230
|
+
for i in 1..identifier.length
|
231
|
+
unless identifier[i..i] =~ /^[a-zA-Z_0-9\-]$/
|
232
|
+
raise ArgumentError,
|
233
|
+
"'" + identifier[i..i] +
|
234
|
+
"' is not a legal character for an SDL identifier. " +
|
235
|
+
"SDL Identifiers must start with a unicode letter or " +
|
236
|
+
"underscore (_) followed by 0 or more unicode " +
|
237
|
+
"letters, digits, underscores (_), or dashes (-)"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,78 @@
|
|
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
|
+
#
|
20
|
+
# Represents a binary value.
|
21
|
+
# This class was introduced to avoid the confusion between a Ruby String and a binary literal.
|
22
|
+
#
|
23
|
+
class SdlBinary
|
24
|
+
|
25
|
+
attr_accessor :bytes
|
26
|
+
|
27
|
+
# +value+: a String containing the bytes
|
28
|
+
def initialize(bytes)
|
29
|
+
@bytes = bytes
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(o)
|
33
|
+
return true if self.equal?(o)
|
34
|
+
return false if not o.instance_of?(self.class)
|
35
|
+
return self.bytes == o.bytes
|
36
|
+
end
|
37
|
+
|
38
|
+
def eql?(o)
|
39
|
+
self == o
|
40
|
+
end
|
41
|
+
|
42
|
+
def hash
|
43
|
+
return bytes.hash
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the bytes base64-encoded.
|
47
|
+
def to_s
|
48
|
+
return Base64.encode64(bytes)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Decodes the specified base-64 encoded string and returns a corresponding SdlBinary
|
52
|
+
# instance.
|
53
|
+
# +s+ might not include the conventional starting and ending square brackets.
|
54
|
+
def self.decode64(s)
|
55
|
+
s = s.delete("\n\r\t ")
|
56
|
+
|
57
|
+
binary = Base64.decode64(s)
|
58
|
+
|
59
|
+
if binary.empty? and not s.empty?
|
60
|
+
raise ArgumentError, "bad binary literal"
|
61
|
+
end
|
62
|
+
|
63
|
+
return SdlBinary.new(binary)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Try to coerce 'o' into a SdlBinary.
|
68
|
+
# Raise an ArgumentError if it fails.
|
69
|
+
def self.SdlBinary(o)
|
70
|
+
if o.kind_of? SdlBinary
|
71
|
+
return o
|
72
|
+
elsif o.kind_of? String
|
73
|
+
return SdlBinary.new(o)
|
74
|
+
else
|
75
|
+
raise ArgumentError, "can't coerce argument"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|