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.
- 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
|