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/README
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
require 'rubygems'
|
7
|
+
|
8
|
+
spec = Gem::Specification.new do |s|
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.summary = "Simple Declarative Language for Ruby library"
|
11
|
+
s.name = 'sdl4r'
|
12
|
+
s.version = '0.9.1'
|
13
|
+
s.requirements << 'none'
|
14
|
+
s.require_path = 'lib'
|
15
|
+
s.author = 'Philippe Vosges'
|
16
|
+
s.email = 'sdl-users@ikayzo.org'
|
17
|
+
s.rubyforge_project = 'sdl4r'
|
18
|
+
s.homepage = 'http://www.ikayzo.org/confluence/display/SDL/Home'
|
19
|
+
s.files = FileList['lib/**/*.rb', 'bin/*', '[A-Z]*', 'test/**/*'].to_a
|
20
|
+
s.test_files = FileList[ 'test/**/*test.rb' ].to_a
|
21
|
+
s.description = <<EOF
|
22
|
+
The Simple Declarative Language provides an easy way to describe lists, maps,
|
23
|
+
and trees of typed data in a compact, easy to read representation.
|
24
|
+
For property files, configuration files, logs, and simple serialization
|
25
|
+
requirements, SDL provides a compelling alternative to XML and Properties
|
26
|
+
files.
|
27
|
+
EOF
|
28
|
+
end
|
29
|
+
|
30
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
31
|
+
pkg.need_zip = true
|
32
|
+
pkg.need_tar = true
|
33
|
+
end
|
34
|
+
|
35
|
+
Rake::RDocTask.new do |rd|
|
36
|
+
rd.rdoc_files.include("lib/**/*.rb")
|
37
|
+
rd.rdoc_dir = "doc"
|
38
|
+
rd.title = "Simple Declarative Language for Ruby"
|
39
|
+
end
|
40
|
+
|
41
|
+
Rake::TestTask.new do |t|
|
42
|
+
t.libs << "lib"
|
43
|
+
t.test_files = FileList['test/**/*test.rb']
|
44
|
+
t.verbose = true
|
45
|
+
end
|
data/TODO.txt
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
[x] Tag.each_attribute should allow to iterate on all namespaces if 'namespace' is nil
|
2
|
+
[x] Tag.get_attributes(namespace)
|
3
|
+
[x] Implementation of attributes=
|
4
|
+
[x] Implementation of Sdl.format()
|
5
|
+
[x] Handle milliseconds in Sdl.format()
|
6
|
+
[x] Handle timezones in Sdl.format()
|
7
|
+
[x] Handle the Time class in Sdl.format()
|
8
|
+
[x] Handle SdlTimeSpan in Sdl.format() ==> via to_s()
|
9
|
+
[x] Review SdlTimeSpan
|
10
|
+
[x] Instead of calling Tag.each_attribute(), we could have another way of calling attributes:
|
11
|
+
with or without given block. Same goes for each_value(), etc.
|
12
|
+
[x] Is Base64 really compatible with the format defined in the Java version ?
|
13
|
+
==> Seems so after having implemented more of the standard tests.
|
14
|
+
[ ] Support both DateTime and Time in parsing (see Parser#combine())
|
15
|
+
[x] Use Date instead of DateTime if only day was specified in SDL (parser.rb)
|
16
|
+
[x] Add a remove_all_children() to Tag
|
17
|
+
[x] Rethink the interfaces to access sub-tags, values, attributes, etc
|
18
|
+
(it is currently difficult to retrieve all the values, or the number of
|
19
|
+
sub-tags, etc)
|
20
|
+
==> to simplify access to values would be ok but attributes or children would be more difficult.
|
21
|
+
[x] See whether there is such a need to shield users from the actual arrays of
|
22
|
+
values (we return a copy for the time being).
|
23
|
+
==> We don't return copies anymore.
|
24
|
+
[ ] Add more unit tests
|
25
|
+
[x] Attribute tests
|
26
|
+
[x] Date tests
|
27
|
+
[x] Date + time test
|
28
|
+
[x] Time zone tests
|
29
|
+
[x] Number literal tests
|
30
|
+
[ ] Strings literals (especially with line continuations)
|
31
|
+
[ ] Sub tags tests
|
32
|
+
[x] "null" value test
|
33
|
+
[x] Comment tests
|
34
|
+
[ ] Bad syntax tests
|
35
|
+
[ ] Test write (unit tests)
|
36
|
+
[ ] Dates
|
37
|
+
[ ] Numbers
|
38
|
+
[ ] Use YARD in order to generate documentation ?
|
39
|
+
[ ] In the documentation, present a table giving the returned Ruby type for each SDL type.
|
40
|
+
[A] Change the interface of SdlTimeSpan to look like the interfaces of Date, DateTime or Time
|
41
|
+
==> Really? This is a timespan, not a date.
|
42
|
+
[x] Have SdlTimeSpan implement Comparable
|
43
|
+
[ ] BUG: the line number is too high by 1 (the column is correct).
|
44
|
+
[x] PB: binary fields shouldn't be kept as Strings because they would not be saved as binaries but
|
45
|
+
as strings otherwise.
|
46
|
+
==> We use SdlBinary now.
|
47
|
+
[x] Change the module name to SDL4R? Something else?
|
48
|
+
==> Changed to SDL4R.
|
49
|
+
[/] Fix the differences between test_basic_types.sdl and what is generated from the parsed structure
|
50
|
+
[x] chars
|
51
|
+
[x] longs
|
52
|
+
[x] doubles
|
53
|
+
[x] decimals
|
54
|
+
[x] booleans
|
55
|
+
[x] null
|
56
|
+
[x] dates
|
57
|
+
[x] times
|
58
|
+
[x] negative times
|
59
|
+
[x] datetimes
|
60
|
+
[/] zone codes
|
61
|
+
==> Time only works in UTC, which means that the original zone code is lost.
|
62
|
+
==> DateTime doesn't give the zone code but only the offset.
|
63
|
+
[ ] Use TzTime? Use a custom object that knows whether a time zone was specified?
|
64
|
+
[x] BUG: Base64 wrapped lines are 64 chars long and not 72 (traditionnal length)
|
65
|
+
==> Because of a limitation in the regular expressions ==> find another algorithm
|
66
|
+
[x] See whether we can do better for numbers of arbitrary precision (decimals).
|
67
|
+
==> Ruby BigDecimal
|
68
|
+
==> Ruby Decimal http://ruby-decimal.rubyforge.org/
|
69
|
+
==> Now: http://flt.rubyforge.org/ : use this if the lib is available
|
70
|
+
==> We use flt if available.
|
71
|
+
[ ] See how Ruby floats relate to Java floats and doubles.
|
72
|
+
[ ] Add tests for the SDL class
|
73
|
+
[ ] Allow unicode characters in identifiers.
|
74
|
+
[ ] It would probably be useful to allow people to replace the standard types by their own. This
|
75
|
+
could be useful for dates or numbers, for instance.
|
76
|
+
[N] To install a gem in the Netbeans gems repository, it needs to run as an administrator.
|
77
|
+
Otherwise, it fails silently.
|
78
|
+
[ ] Make it so that the tests pass (with errors or not), when Ftl (DecNum) is not available.
|
79
|
+
[x] Fix the ParserTest test.
|
80
|
+
[x] SDL 1.1: tag1; tag2 "a value"; tag3 name="foo"
|
81
|
+
[x] Create a Tokenizer class
|
82
|
+
[A] Test attributes with/without namespaces
|
83
|
+
==> Guess this is done in test_structures...
|
84
|
+
[ ] Propose to Dan that the top level is considered as a root tag that can't have values but
|
85
|
+
just attributes and sub-tags.
|
86
|
+
[x] Fix Test.test_strings: support for Unicode
|
87
|
+
==> Seems to work.
|
88
|
+
[ ] Consider being able to read text files that are not UTF-8(?)
|
89
|
+
[ ] BUG: the report on the line no in errors is off by 1 (at least in some cases)
|
90
|
+
[ ] Try to move Reader, Tokenizer, etc to the private part of the SDL module
|
91
|
+
==> Doesn't seem to make a difference.
|
92
|
+
[x] Factorize "each_child..." methods in Tag and refactor "children(recursive, name)" consequently
|
93
|
+
+ invert "name" and "recursive" in "children()"
|
94
|
+
[ ] Return copies or original arrays in Tag?
|
95
|
+
[ ] BUG: test_tag_write_parse() does not work from the command line (ruby v1.8.7).
|
96
|
+
[ ] Tag.to_string(): break up long lines using the backslash
|
97
|
+
[ ] Tag.hash: the implementation is not very efficient.
|
98
|
+
[ ] Implement reading from a URL(?) Other sources idiomatic in Ruby?
|
99
|
+
[ ] See the XML and YAML APIs to find enhancements.
|
100
|
+
[ ] Check the XML functionalities of Tag.
|
101
|
+
[ ] Add tests for Tag
|
102
|
+
[ ] Maybe some methods in the SDL module are not so useful to the general user: make them protected?
|
103
|
+
[ ] FUTURE: xpath, ypath ==> sdlpath(?)
|
104
|
+
[ ] FUTURE: evenemential parsing(?)
|
105
|
+
[ ] Move Tokenizer, Reader, etc into the Parser module/class or prefix by Sdl
|
106
|
+
[ ] FUTURE: add a way to insert a tag after or before another(?)
|
107
|
+
[ ] FUTURE: allow some way of generating YAML(?)
|
108
|
+
[ ] FUTURE: allow to turn a YAML structure into a SDL one(?)
|
109
|
+
[ ] Make a Gem
|
110
|
+
[ ] Implement the convenience method of SDL (value(), list(), map())
|
111
|
+
[x] Move the test files to a sdl4r subdir
|
112
|
+
[ ] 1.2: Ensure that there is a SDL.read() that returns a Tag
|
113
|
+
[ ] 1.2: hasChild(), hasChildren(), getChildMap(), getChildStringMap() methods
|
114
|
+
[ ] 1.3: periods in identifiers
|
115
|
+
[ ] Create a History.txt file
|
116
|
+
[ ] Setup the Rubyforge website
|
117
|
+
[ ] See how both Subversion repositories can be handled (is it necessary?)
|
data/lib/scratchpad.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
if RUBY_VERSION < '1.9.0'
|
2
|
+
$KCODE = 'u'
|
3
|
+
require 'jcode'
|
4
|
+
end
|
5
|
+
|
6
|
+
#require "rubygems"
|
7
|
+
require "flt"
|
8
|
+
#
|
9
|
+
#puts "DecNum=" + Flt::DecNum("12345678901234567890").to_s
|
10
|
+
#puts "DecNum precision=" + Flt::DecNum.context.precision.to_s
|
11
|
+
|
12
|
+
puts "Flt::DecNum available" if defined? Flt::DecNum
|
13
|
+
|
14
|
+
require 'time'
|
15
|
+
require File.dirname(__FILE__) + '/sdl4r/sdl'
|
16
|
+
require File.dirname(__FILE__) + '/sdl4r/tag'
|
17
|
+
|
18
|
+
#if "+09:00" =~ /(?:-([a-zA-Z]+))|(?:([\+\-]\d+)(?::(\d+))?)/
|
19
|
+
# puts "matches " + $1.to_s + " " + $2.to_s
|
20
|
+
#end
|
21
|
+
|
22
|
+
#if "03:00-UTC-04" =~ /^([+-]?\d+):(\d+)(?::(\d+)(?:\.(\d+))?)?(?:(?:-([a-zA-Z]+))?(?:([\+\-]\d+)(?::(\d+))?)?)?$/i
|
23
|
+
# puts $~
|
24
|
+
#end
|
25
|
+
|
26
|
+
root = SDL4R::Tag.new("root")
|
27
|
+
#open("D:\\dev\\sdl\\sdl4r\\test\\test_structures.sdl") do |io|
|
28
|
+
# root.read(io)
|
29
|
+
#end
|
30
|
+
##root.read(
|
31
|
+
##<<EOF
|
32
|
+
##matrix {
|
33
|
+
# 1 2 3
|
34
|
+
# 4 5 6
|
35
|
+
#}
|
36
|
+
##EOF
|
37
|
+
##)
|
38
|
+
root.read(
|
39
|
+
<<EOF
|
40
|
+
toto titi=null tata=2
|
41
|
+
EOF
|
42
|
+
)
|
43
|
+
#local_offset = DateTime.now.offset
|
44
|
+
#puts "local_offset=#{local_offset * 24}"
|
45
|
+
#puts DateTime.civil(1980,12,5,12,30,0,local_offset)
|
46
|
+
|
47
|
+
root.children { |child| puts child.to_s }
|
48
|
+
|
49
|
+
puts root.to_s
|
data/lib/sdl4r/parser.rb
ADDED
@@ -0,0 +1,678 @@
|
|
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 'base64'
|
20
|
+
|
21
|
+
begin
|
22
|
+
# Try to use the Flt library, which defines DecNum
|
23
|
+
require "flt"
|
24
|
+
rescue LoadError
|
25
|
+
# Well, shouganai.
|
26
|
+
end
|
27
|
+
|
28
|
+
require File.dirname(__FILE__) + '/sdl_binary'
|
29
|
+
require File.dirname(__FILE__) + '/sdl_time_span'
|
30
|
+
require File.dirname(__FILE__) + '/sdl_parse_error'
|
31
|
+
require File.dirname(__FILE__) + '/tokenizer'
|
32
|
+
|
33
|
+
# The SDL parser.
|
34
|
+
#
|
35
|
+
# Authors: Daniel Leuck, Philippe Vosges
|
36
|
+
#
|
37
|
+
# In Ruby 1.8, in order to enable UTF-8 support, you may have to declare the following lines:
|
38
|
+
#
|
39
|
+
# $KCODE = 'u'
|
40
|
+
# require 'jcode'
|
41
|
+
#
|
42
|
+
# This will give you correct input and output and correct UTF-8 "general" sorting.
|
43
|
+
# Alternatively you can use the following options when launching the Ruby interpreter:
|
44
|
+
#
|
45
|
+
# /path/to/ruby -Ku -rjcode
|
46
|
+
#
|
47
|
+
class Parser
|
48
|
+
|
49
|
+
# Passed to parse_error() in order to specify an error that occured on no specific position
|
50
|
+
# (column).
|
51
|
+
UNKNOWN_POSITION = -2
|
52
|
+
|
53
|
+
# Creates an SDL parser on the specified +IO+.
|
54
|
+
def initialize(io)
|
55
|
+
raise ArgumentError, "io == nil" if io.nil?
|
56
|
+
|
57
|
+
@tokenizer = Tokenizer.new(io)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Parses the underlying +IO+ and returns an +Array+ of +Tag+.
|
61
|
+
#
|
62
|
+
# ==Errors
|
63
|
+
# [IOError] If a problem is encountered with the IO
|
64
|
+
# [SdlParseError] If the document is malformed
|
65
|
+
def parse
|
66
|
+
tags = []
|
67
|
+
|
68
|
+
while tokens = @tokenizer.read_line_tokens()
|
69
|
+
if tokens.last.type == :START_BLOCK
|
70
|
+
# tag with a block
|
71
|
+
tag = construct_tag(tokens[0...-1])
|
72
|
+
add_children(tag)
|
73
|
+
tags << tag
|
74
|
+
|
75
|
+
elsif tokens.first.type == :END_BLOCK
|
76
|
+
# we found an block end token that should have been consumed by
|
77
|
+
# add_children() normally
|
78
|
+
parse_error(
|
79
|
+
"No opening block ({) for close block (}).",
|
80
|
+
tokens.first.line,
|
81
|
+
tokens.first.position)
|
82
|
+
else
|
83
|
+
# tag without block
|
84
|
+
tags << construct_tag(tokens)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
@tokenizer.close()
|
89
|
+
|
90
|
+
return tags
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Parses the children tags of +parent+ until an end of block is found.
|
96
|
+
def add_children(parent)
|
97
|
+
while tokens = @tokenizer.read_line_tokens()
|
98
|
+
if tokens.first.type == :END_BLOCK
|
99
|
+
return
|
100
|
+
|
101
|
+
elsif tokens.last.type == :START_BLOCK
|
102
|
+
# found a child with a block
|
103
|
+
tag = construct_tag(tokens[0...-1]);
|
104
|
+
add_children(tag)
|
105
|
+
parent.add_child(tag)
|
106
|
+
|
107
|
+
else
|
108
|
+
parent.add_child(construct_tag(tokens))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
parse_error("No close block (}).", @tokenizer.line_no, UNKNOWN_POSITION)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Construct a Tag (but not its children) from a string of tokens
|
116
|
+
#
|
117
|
+
# Throws SdlParseError if some bad syntax is found.
|
118
|
+
def construct_tag(tokens)
|
119
|
+
raise ArgumentError, "tokens == nil" if tokens.nil?
|
120
|
+
if tokens.empty?
|
121
|
+
parse_error("Internal Error: empty token list", @tokenizer.line_no, UNKNOWN_POSITION)
|
122
|
+
end
|
123
|
+
|
124
|
+
first_token = tokens.first
|
125
|
+
if first_token.literal?
|
126
|
+
first_token = Token.new("content")
|
127
|
+
tokens.insert(0, first_token)
|
128
|
+
|
129
|
+
elsif first_token.type != :IDENTIFIER
|
130
|
+
expecting_but_got(
|
131
|
+
"IDENTIFIER",
|
132
|
+
"#{first_token.type} (#{first_token.text})",
|
133
|
+
first_token.line,
|
134
|
+
first_token.position)
|
135
|
+
end
|
136
|
+
|
137
|
+
tag = nil
|
138
|
+
if tokens.size == 1
|
139
|
+
tag = Tag.new(first_token.text)
|
140
|
+
|
141
|
+
else
|
142
|
+
values_start_index = 1
|
143
|
+
second_token = tokens[1]
|
144
|
+
|
145
|
+
if second_token.type == :COLON
|
146
|
+
if tokens.size == 2 or tokens[2].type != :IDENTIFIER
|
147
|
+
parse_error(
|
148
|
+
"Colon (:) encountered in unexpected location.",
|
149
|
+
second_token.line,
|
150
|
+
second_token.position)
|
151
|
+
end
|
152
|
+
|
153
|
+
third_token = tokens[2];
|
154
|
+
tag = Tag.new(third_token.text, first_token.text)
|
155
|
+
values_start_index = 3
|
156
|
+
|
157
|
+
else
|
158
|
+
tag = Tag.new(first_token.text)
|
159
|
+
end
|
160
|
+
|
161
|
+
# read values
|
162
|
+
attribute_start_index = add_tag_values(tag, tokens, values_start_index)
|
163
|
+
|
164
|
+
# read attributes
|
165
|
+
if attribute_start_index < tokens.size
|
166
|
+
add_tag_attributes(tag, tokens, attribute_start_index)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
return tag
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# @return The position at the end of the value list
|
175
|
+
#
|
176
|
+
def add_tag_values(tag, tokens, start)
|
177
|
+
size = tokens.size()
|
178
|
+
i = start;
|
179
|
+
|
180
|
+
while i < size
|
181
|
+
token = tokens[i]
|
182
|
+
|
183
|
+
if token.literal?
|
184
|
+
# if a DATE token is followed by a TIME token combine them
|
185
|
+
next_token = ((i + 1) < size)? tokens[i + 1] : nil
|
186
|
+
if token.type == :DATE && next_token && next_token.type == :TIME
|
187
|
+
date = token.object_for_literal()
|
188
|
+
time_zone_with_zone = next_token.object_for_literal()
|
189
|
+
|
190
|
+
if time_zone_with_zone.day != 0
|
191
|
+
# as there are days specified, it can't be a full precision date
|
192
|
+
tag.add_value(date);
|
193
|
+
tag.add_value(
|
194
|
+
SdlTimeSpan.new(
|
195
|
+
time_zone_with_zone.day,
|
196
|
+
time_zone_with_zone.hour,
|
197
|
+
time_zone_with_zone.min,
|
198
|
+
time_zone_with_zone.sec))
|
199
|
+
|
200
|
+
|
201
|
+
if time_zone_with_zone.time_zone_offset
|
202
|
+
parse_error("TimeSpan cannot have a timeZone", t.line, t.position)
|
203
|
+
end
|
204
|
+
|
205
|
+
else
|
206
|
+
tag.add_value(combine(date, time_zone_with_zone))
|
207
|
+
end
|
208
|
+
|
209
|
+
i += 1
|
210
|
+
|
211
|
+
else
|
212
|
+
value = token.object_for_literal()
|
213
|
+
if value.is_a?(TimeSpanWithZone)
|
214
|
+
# the literal looks like a time zone
|
215
|
+
if value.time_zone_offset
|
216
|
+
expecting_but_got(
|
217
|
+
"TIME SPAN",
|
218
|
+
"TIME (component of date/time)",
|
219
|
+
token.line,
|
220
|
+
token.position)
|
221
|
+
end
|
222
|
+
|
223
|
+
tag.add_value(
|
224
|
+
SdlTimeSpan.new(
|
225
|
+
value.day,
|
226
|
+
value.hour,
|
227
|
+
value.min,
|
228
|
+
value.sec))
|
229
|
+
else
|
230
|
+
tag.add_value(value)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
elsif token.type == :IDENTIFIER
|
234
|
+
break
|
235
|
+
else
|
236
|
+
expecting_but_got(
|
237
|
+
"LITERAL or IDENTIFIER", token.type, token.line, token.position)
|
238
|
+
end
|
239
|
+
|
240
|
+
i += 1
|
241
|
+
end
|
242
|
+
|
243
|
+
return i
|
244
|
+
end
|
245
|
+
|
246
|
+
#
|
247
|
+
# Add attributes to the given tag
|
248
|
+
#
|
249
|
+
def add_tag_attributes(tag, tokens, start)
|
250
|
+
i = start
|
251
|
+
size = tokens.size
|
252
|
+
|
253
|
+
while i < size
|
254
|
+
token = tokens[i]
|
255
|
+
if token.type != :IDENTIFIER
|
256
|
+
expecting_but_got("IDENTIFIER", token.type, token.line, token.position)
|
257
|
+
end
|
258
|
+
name_or_namespace = token.text;
|
259
|
+
|
260
|
+
if i == (size - 1)
|
261
|
+
expecting_but_got(
|
262
|
+
"\":\" or \"=\" \"LITERAL\"",
|
263
|
+
"END OF LINE.",
|
264
|
+
token.line,
|
265
|
+
token.position)
|
266
|
+
end
|
267
|
+
|
268
|
+
i += 1
|
269
|
+
token = tokens[i]
|
270
|
+
if token.type == :COLON
|
271
|
+
if i == (size - 1)
|
272
|
+
expecting_but_got(
|
273
|
+
"IDENTIFIER", "END OF LINE", token.line, token.position)
|
274
|
+
end
|
275
|
+
|
276
|
+
i += 1
|
277
|
+
token = tokens[i]
|
278
|
+
if token.type != :IDENTIFIER
|
279
|
+
expecting_but_got(
|
280
|
+
"IDENTIFIER", token.type, token.line, token.position)
|
281
|
+
end
|
282
|
+
name = token.text
|
283
|
+
|
284
|
+
if i == (size - 1)
|
285
|
+
expecting_but_got("\"=\"", "END OF LINE", token.line, token.position)
|
286
|
+
end
|
287
|
+
|
288
|
+
i += 1
|
289
|
+
token = tokens[i]
|
290
|
+
if token.type != :EQUALS
|
291
|
+
expecting_but_got("\"=\"", token.type, token.line, token.position)
|
292
|
+
end
|
293
|
+
|
294
|
+
if i == (size - 1)
|
295
|
+
expecting_but_got("LITERAL", "END OF LINE", token.line, token.position)
|
296
|
+
end
|
297
|
+
|
298
|
+
i += 1
|
299
|
+
token = tokens[i]
|
300
|
+
if !token.literal?
|
301
|
+
expecting_but_got("LITERAL", token.type, token.line, token.position)
|
302
|
+
end
|
303
|
+
|
304
|
+
if token.type == :DATE and (i + 1) < size and tokens[i + 1].type == :TIME
|
305
|
+
date = token.get_object_for_literal()
|
306
|
+
time_span_with_zone = tokens[i + 1].get_object_for_literal()
|
307
|
+
|
308
|
+
if time_span_with_zone.days != 0
|
309
|
+
expecting_but_got(
|
310
|
+
"TIME (component of date/time) in attribute value",
|
311
|
+
"TIME SPAN",
|
312
|
+
token.line,
|
313
|
+
token.position)
|
314
|
+
else
|
315
|
+
tag.set_attribute(
|
316
|
+
name, combine(date, time_span_with_zone), name_or_namespace)
|
317
|
+
end
|
318
|
+
|
319
|
+
i += 1
|
320
|
+
else
|
321
|
+
value = token.object_for_literal();
|
322
|
+
if value.is_a?(TimeSpanWithZone)
|
323
|
+
time_span_with_zone = value
|
324
|
+
|
325
|
+
if time_span_with_zone.time_zone_offset
|
326
|
+
expecting_but_got(
|
327
|
+
"TIME SPAN",
|
328
|
+
"TIME (component of date/time)",
|
329
|
+
token.line,
|
330
|
+
token.position)
|
331
|
+
end
|
332
|
+
|
333
|
+
time_span = SdlTimeSpan.new(
|
334
|
+
time_span_with_zone.day,
|
335
|
+
time_span_with_zone.hour,
|
336
|
+
time_span_with_zone.min,
|
337
|
+
time_span_with_zone.sec)
|
338
|
+
|
339
|
+
tag.set_attribute(name, time_span, name_or_namespace)
|
340
|
+
else
|
341
|
+
tag.set_attribute(name, value, name_or_namespace);
|
342
|
+
end
|
343
|
+
end
|
344
|
+
elsif token.type == :EQUALS
|
345
|
+
if i == (size - 1)
|
346
|
+
expecting_but_got("LITERAL", "END OF LINE", token.line, token.position)
|
347
|
+
end
|
348
|
+
|
349
|
+
i += 1
|
350
|
+
token = tokens[i]
|
351
|
+
if !token.literal?
|
352
|
+
expecting_but_got("LITERAL", token.type, token.line, token.position)
|
353
|
+
end
|
354
|
+
|
355
|
+
if token.type == :DATE and (i + 1) < size and tokens[i + 1].type == :TIME
|
356
|
+
date = token.object_for_literal()
|
357
|
+
time_span_with_zone = tokens[i + 1].object_for_literal()
|
358
|
+
|
359
|
+
if time_span_with_zone.day != 0
|
360
|
+
expecting_but_got(
|
361
|
+
"TIME (component of date/time) in attribute value",
|
362
|
+
"TIME SPAN",
|
363
|
+
token.line,
|
364
|
+
token.position)
|
365
|
+
end
|
366
|
+
tag.set_attribute(
|
367
|
+
name_or_namespace, combine(date, time_span_with_zone))
|
368
|
+
|
369
|
+
i += 1
|
370
|
+
else
|
371
|
+
value = token.object_for_literal()
|
372
|
+
if value.is_a?(TimeSpanWithZone)
|
373
|
+
time_span_with_zone = value
|
374
|
+
if time_span_with_zone.time_zone_offset
|
375
|
+
expecting_but_got(
|
376
|
+
"TIME SPAN",
|
377
|
+
"TIME (component of date/time)",
|
378
|
+
token.line,
|
379
|
+
token.position)
|
380
|
+
end
|
381
|
+
|
382
|
+
time_span = SdlTimeSpan.new(
|
383
|
+
time_span_with_zone.day,
|
384
|
+
time_span_with_zone.hour,
|
385
|
+
time_span_with_zone.min,
|
386
|
+
time_span_with_zone.sec)
|
387
|
+
tag.set_attribute(name_or_namespace, time_span)
|
388
|
+
else
|
389
|
+
tag.set_attribute(name_or_namespace, value);
|
390
|
+
end
|
391
|
+
end
|
392
|
+
else
|
393
|
+
expecting_but_got(
|
394
|
+
"\":\" or \"=\"", token.type, token.line, token.position)
|
395
|
+
end
|
396
|
+
|
397
|
+
i += 1
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# Combines a simple Date with a TimeSpanWithZone to create a DateTime
|
402
|
+
#
|
403
|
+
def combine(date, time_span_with_zone)
|
404
|
+
time_zone_offset = time_span_with_zone.time_zone_offset
|
405
|
+
time_zone_offset = TimeSpanWithZone.default_time_zone_offset if time_zone_offset.nil?
|
406
|
+
|
407
|
+
return DateTime.new(
|
408
|
+
date.year,
|
409
|
+
date.month,
|
410
|
+
date.day,
|
411
|
+
time_span_with_zone.hour,
|
412
|
+
time_span_with_zone.min,
|
413
|
+
time_span_with_zone.sec,
|
414
|
+
time_zone_offset)
|
415
|
+
end
|
416
|
+
|
417
|
+
# An intermediate object used to store a timeSpan or the time
|
418
|
+
# component of a date/time instance. The types are disambiguated at a later stage.
|
419
|
+
#
|
420
|
+
# +seconds+ can have a fraction
|
421
|
+
# +time_zone_offset+ is a fraction of a day (equal to nil if not specified)
|
422
|
+
class TimeSpanWithZone
|
423
|
+
|
424
|
+
private
|
425
|
+
|
426
|
+
SECONDS_IN_DAY = 24 * 60 * 60
|
427
|
+
|
428
|
+
public
|
429
|
+
|
430
|
+
def initialize(day, hour, minute, second, time_zone_offset)
|
431
|
+
@day = day
|
432
|
+
@hour = hour
|
433
|
+
@min = minute
|
434
|
+
@sec = second
|
435
|
+
@time_zone_offset = time_zone_offset
|
436
|
+
end
|
437
|
+
|
438
|
+
attr_reader :day, :hour, :min, :sec, :time_zone_offset
|
439
|
+
|
440
|
+
# Returns the UTC offset as a fraction of a day on the current machine
|
441
|
+
def TimeSpanWithZone.default_time_zone_offset
|
442
|
+
return Rational(Time.now.utc_offset, SECONDS_IN_DAY)
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
private
|
447
|
+
############################################################################
|
448
|
+
## Parsers for types
|
449
|
+
############################################################################
|
450
|
+
|
451
|
+
def Parser.parse_string(literal)
|
452
|
+
unless literal =~ /(^`.*`$)|(^\".*\"$)/m
|
453
|
+
raise ArgumentError,
|
454
|
+
"Malformed string <#{literal}>." +
|
455
|
+
" Strings must start and end with \" or `"
|
456
|
+
end
|
457
|
+
|
458
|
+
return literal[1..-2]
|
459
|
+
end
|
460
|
+
|
461
|
+
def Parser.parse_character(literal)
|
462
|
+
unless literal =~ /(^'.*'$)/
|
463
|
+
raise ArgumentError,
|
464
|
+
"Malformed character <#{literal}>." +
|
465
|
+
" Character must start and end with single quotes"
|
466
|
+
end
|
467
|
+
|
468
|
+
return literal[1]
|
469
|
+
end
|
470
|
+
|
471
|
+
def Parser.parse_number(literal)
|
472
|
+
# we use the fact that Kernel.Integer() and Kernel.Float() raise ArgumentErrors
|
473
|
+
if literal =~ /(.*)(L)$/i
|
474
|
+
return Integer($1)
|
475
|
+
elsif literal =~ /([^BDF]*)(BD)$/i
|
476
|
+
return (defined? Flt::DecNum) ? Flt::DecNum($1) : Float($1)
|
477
|
+
elsif literal =~ /([^BDF]*)(F|D)$/i
|
478
|
+
return Float($1)
|
479
|
+
elsif literal.count(".e") == 0
|
480
|
+
return Integer(literal)
|
481
|
+
else
|
482
|
+
return Float(literal)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
# Parses the given literal into a returned array
|
487
|
+
# [days, hours, minutes, seconds, time_zone_offset].
|
488
|
+
# 'days', 'hours' and 'minutes' are integers.
|
489
|
+
# 'seconds' and 'time_zone_offset' are rational numbers.
|
490
|
+
# 'days' and 'seconds' are equal to 0 if they're not specified in ((|literal|)).
|
491
|
+
# 'time_zone_offset' is equal to nil if not specified.
|
492
|
+
#
|
493
|
+
# ((|allowDays|)) indicates whether the specification of days is allowed
|
494
|
+
# in ((|literal|))
|
495
|
+
# ((|allowTimeZone|)) indicates whether the specification of the timeZone is
|
496
|
+
# allowed in ((|literal|))
|
497
|
+
#
|
498
|
+
# All components are returned disregarding the values of ((|allowDays|)) and
|
499
|
+
# ((|allowTimeZone|)).
|
500
|
+
#
|
501
|
+
# Raises an ArgumentError if ((|literal|)) has a bad format.
|
502
|
+
def Parser.parse_time_span_and_time_zone(literal, allowDays, allowTimeZone)
|
503
|
+
overall_sign = (literal =~ /^-/)? -1 : +1
|
504
|
+
|
505
|
+
if literal =~ /^(([+\-]?\d+)d:)/
|
506
|
+
if allowDays
|
507
|
+
days = Integer($2)
|
508
|
+
days_specified = true
|
509
|
+
time_part = literal[($1.length)..-1]
|
510
|
+
else
|
511
|
+
# detected a day specification in a pure time literal
|
512
|
+
raise ArgumentError, "unexpected day specification in #{literal}"
|
513
|
+
end
|
514
|
+
else
|
515
|
+
days = 0;
|
516
|
+
days_specified = false
|
517
|
+
time_part = literal
|
518
|
+
end
|
519
|
+
|
520
|
+
# We have to parse the string ourselves because AFAIK :
|
521
|
+
# - strptime() can't parse milliseconds
|
522
|
+
# - strptime() can't parse the time zone custom offset (CET+02:30)
|
523
|
+
# - strptime() accepts trailing chars
|
524
|
+
# (e.g. "12:24-xyz@" ==> "xyz@" is obviously wrong but strptime()
|
525
|
+
# won't mind)
|
526
|
+
if time_part =~ /^([+-]?\d+):(\d+)(?::(\d+)(?:\.(\d+))?)?(?:(?:-([a-zA-Z]+))?(?:([\+\-]\d+)(?::(\d+))?)?)?$/i
|
527
|
+
hours = $1.to_i
|
528
|
+
minutes = $2.to_i
|
529
|
+
# seconds and milliseconds are implemented as one rational number
|
530
|
+
# unless there are no milliseconds
|
531
|
+
millisecond_part = ($4)? $4.ljust(3, "0") : nil
|
532
|
+
if millisecond_part
|
533
|
+
seconds = Rational(($3 + millisecond_part).to_i, 10 ** millisecond_part.length)
|
534
|
+
else
|
535
|
+
seconds = ($3)? Integer($3) : 0
|
536
|
+
end
|
537
|
+
|
538
|
+
if ($5 or $6) and not allowTimeZone
|
539
|
+
raise ArgumentError, "unexpected time zone specification in #{literal}"
|
540
|
+
end
|
541
|
+
|
542
|
+
time_zone_code = $5 # might be nil
|
543
|
+
|
544
|
+
if $6
|
545
|
+
zone_custom_minute_offset = $6.to_i * 60
|
546
|
+
if $7
|
547
|
+
if zone_custom_minute_offset > 0
|
548
|
+
zone_custom_minute_offset = zone_custom_minute_offset + $7.to_i
|
549
|
+
else
|
550
|
+
zone_custom_minute_offset = zone_custom_minute_offset - $7.to_i
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
time_zone_offset = get_time_zone_offset(time_zone_code, zone_custom_minute_offset)
|
556
|
+
|
557
|
+
if not allowDays and $1 =~ /^[+-]/
|
558
|
+
# unexpected timeSpan syntax
|
559
|
+
raise ArgumentError, "unexpected sign on hours : #{literal}"
|
560
|
+
end
|
561
|
+
|
562
|
+
# take the sign into account
|
563
|
+
hours *= overall_sign if days_specified # otherwise the sign is already applied to the hours
|
564
|
+
minutes *= overall_sign
|
565
|
+
seconds *= overall_sign
|
566
|
+
|
567
|
+
return [ days, hours, minutes, seconds, time_zone_offset ]
|
568
|
+
|
569
|
+
else
|
570
|
+
raise ArgumentError, "bad time component : #{literal}"
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
# Parses the given literal (String) into a returned DateTime object.
|
575
|
+
#
|
576
|
+
# Raises an ArgumentError if ((|literal|)) has a bad format.
|
577
|
+
def Parser.parse_date_time(literal)
|
578
|
+
raise ArgumentError("date literal is nil") if literal.nil?
|
579
|
+
|
580
|
+
begin
|
581
|
+
parts = literal.split(" ")
|
582
|
+
if parts.length == 1
|
583
|
+
return parse_date(literal)
|
584
|
+
else
|
585
|
+
date = parse_date(parts[0]);
|
586
|
+
time_part = parts[1]
|
587
|
+
|
588
|
+
days, hours, minutes, seconds, time_zone_offset =
|
589
|
+
parse_time_span_and_time_zone(time_part, false, true)
|
590
|
+
|
591
|
+
return DateTime.civil(
|
592
|
+
date.year,
|
593
|
+
date.month,
|
594
|
+
date.day,
|
595
|
+
hours,
|
596
|
+
minutes,
|
597
|
+
seconds,
|
598
|
+
time_zone_offset)
|
599
|
+
end
|
600
|
+
|
601
|
+
rescue ArgumentError
|
602
|
+
raise ArgumentError, "Bad date/time #{literal} : #{$!.message}"
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
##
|
607
|
+
# Returns the time zone offset (Rational) corresponding to the provided parameters as a fraction
|
608
|
+
# of a day. This method adds the two offsets if they are both provided.
|
609
|
+
#
|
610
|
+
# +time_zone_code+: can be nil
|
611
|
+
# +custom_minute_offset+: can be nil
|
612
|
+
#
|
613
|
+
def Parser.get_time_zone_offset(time_zone_code, custom_minute_offset)
|
614
|
+
return nil unless time_zone_code or custom_minute_offset
|
615
|
+
|
616
|
+
time_zone_offset = custom_minute_offset ? Rational(custom_minute_offset, 60 * 24) : 0
|
617
|
+
|
618
|
+
return time_zone_offset unless time_zone_code
|
619
|
+
|
620
|
+
# we have to provide some bogus year/month/day in order to parse our time zone code
|
621
|
+
d = DateTime.strptime("1999/01/01 #{time_zone_code}", "%Y/%m/%d %Z")
|
622
|
+
# the offset is a fraction of a day
|
623
|
+
return d.offset() + time_zone_offset
|
624
|
+
end
|
625
|
+
|
626
|
+
# Parses the +literal+ into a returned Date object.
|
627
|
+
#
|
628
|
+
# Raises an ArgumentError if +literal+ has a bad format.
|
629
|
+
|
630
|
+
def Parser.parse_date(literal)
|
631
|
+
# here, we're being stricter than strptime() alone as we forbid trailing chars
|
632
|
+
if literal =~ /^(\d+)\/(\d+)\/(\d+)$/
|
633
|
+
begin
|
634
|
+
return Date.strptime(literal, "%Y/%m/%d")
|
635
|
+
rescue ArgumentError
|
636
|
+
raise ArgumentError, "Malformed Date <#{literal}> : #{$!.message}"
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
raise ArgumentError, "Malformed Date <#{literal}>"
|
641
|
+
end
|
642
|
+
|
643
|
+
# Returns a String that contains the binary content corresponding to ((|literal|)).
|
644
|
+
#
|
645
|
+
# ((|literal|)) : a base-64 encoded literal (e.g.
|
646
|
+
# "[V2hvIHdhbnRzIHRvIGxpdmUgZm9yZXZlcj8=]")
|
647
|
+
def Parser.parse_binary(literal)
|
648
|
+
clean_literal = literal[1..-2] # remove square brackets
|
649
|
+
return SdlBinary.decode64(clean_literal)
|
650
|
+
end
|
651
|
+
|
652
|
+
# Parses +literal+ (String) into the corresponding SDLTimeSpan, which is then
|
653
|
+
# returned.
|
654
|
+
#
|
655
|
+
# Raises an ArgumentError if the literal is not a correct timeSpan literal.
|
656
|
+
def Parser.parse_time_span(literal)
|
657
|
+
days, hours, minutes, seconds, time_zone_offset =
|
658
|
+
parse_time_span_and_time_zone(literal, true, false)
|
659
|
+
|
660
|
+
milliseconds = ((seconds - seconds.to_i) * 1000).to_i
|
661
|
+
seconds = seconds.to_i
|
662
|
+
|
663
|
+
return SDLTimeSpan.new(days, hours, minutes, seconds, milliseconds)
|
664
|
+
|
665
|
+
raise ArgumentError,
|
666
|
+
"Malformed time span <#{literal}>. Time spans must use the format " +
|
667
|
+
"(d:)hh:mm:ss(.xxx) Note: if the day component is " +
|
668
|
+
"included it must be suffixed with lower case \"d\""
|
669
|
+
end
|
670
|
+
|
671
|
+
# Close the reader and throw a SdlParseError using the format
|
672
|
+
# Was expecting X but got Y.
|
673
|
+
#
|
674
|
+
def expecting_but_got(expecting, got, line, position)
|
675
|
+
@tokenizer.expecting_but_got(expecting, got, line, position)
|
676
|
+
end
|
677
|
+
end
|
678
|
+
end
|