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,44 @@
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
+ # An exception describing a problem with an SDL document's structure
20
+ #
21
+ class SdlParseError < StandardError
22
+
23
+ #
24
+ # Note: Line and positioning numbering start with 1 rather than 0 to be
25
+ # consistent with most editors.
26
+ #
27
+ # +description+ A description of the problem.
28
+ # +lineNo+ The line on which the error occured or -1 for unknown
29
+ # +position+ The position (within the line) where the error occured or -1 for unknown
30
+ #
31
+ def initialize(description, line_no, position, line = nil)
32
+ super(
33
+ "#{description} Line " + ((line_no.nil? or line_no < 0)? "unknown" : line_no.to_s) +
34
+ ", Position " + ((position.nil? or position < 0)? "unknown" : position.to_s) + "\n" +
35
+ (line ? line + (position ? " " * (position - 1) : "") + "^" : ""))
36
+
37
+ @lineNo = line_no
38
+ @position = position
39
+ end
40
+
41
+ attr_reader :line
42
+ attr_reader :position
43
+ end
44
+ end
@@ -0,0 +1,301 @@
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
+ # Represents a period of time (duration) as opposed to a particular
20
+ # moment in time (which would be represented using a Date, DateTime or Time
21
+ # instance).
22
+ #
23
+ class SdlTimeSpan
24
+ include Comparable
25
+
26
+ private
27
+
28
+ MILLISECONDS_IN_SECOND = 1000
29
+ MILLISECONDS_IN_MINUTE = 60 * MILLISECONDS_IN_SECOND
30
+ MILLISECONDS_IN_HOUR = 60 * MILLISECONDS_IN_MINUTE
31
+ MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR
32
+
33
+
34
+ # Initializes an SdlTimeSpan using the total number of milliseconds in the
35
+ # span.
36
+ #
37
+ def initialize_total_milliseconds(total_milliseconds)
38
+ @totalMilliseconds = total_milliseconds
39
+ end
40
+
41
+ # Initializes an SdlTimeSpan defined by its day, hour, minute and
42
+ # millisecond parts.
43
+ # Note: if the timespan is negative all components should be negative.
44
+ #
45
+ def initialize_days_hours_minutes(days, hours, minutes, seconds = 0, milliseconds = 0)
46
+ if seconds.is_a?(Rational)
47
+ s = seconds.truncate
48
+ milliseconds = milliseconds + ((seconds - s) * 1000).round
49
+ seconds = s
50
+ end
51
+
52
+ @totalMilliseconds =
53
+ days * MILLISECONDS_IN_DAY +
54
+ hours * MILLISECONDS_IN_HOUR +
55
+ minutes * MILLISECONDS_IN_MINUTE +
56
+ seconds * MILLISECONDS_IN_SECOND +
57
+ milliseconds
58
+ end
59
+
60
+ public
61
+
62
+ # Create an SdlTimeSpan. Note: if the timespan is negative all
63
+ # components should be negative.
64
+ #
65
+ # SdlTimeSpan.new(days, hours, minutes, seconds = 0, milliseconds = 0)
66
+ #
67
+ # or
68
+ #
69
+ # SdlTimeSpan.new(totalMilliseconds)
70
+ #
71
+ def initialize(*args)
72
+ if args.length == 1
73
+ initialize_total_milliseconds(args[0])
74
+ else
75
+ initialize_days_hours_minutes(*args)
76
+ end
77
+ end
78
+
79
+ # Returns the sign (-1 or +1) of this SdlTimeSpan.
80
+ #
81
+ def sign
82
+ @totalMilliseconds <=> 0
83
+ end
84
+
85
+ # The days component.
86
+ #
87
+ def days
88
+ sign * (@totalMilliseconds.abs / MILLISECONDS_IN_DAY)
89
+ end
90
+
91
+ # The hours component.
92
+ #
93
+ def hours
94
+ return sign * ((@totalMilliseconds - (days * MILLISECONDS_IN_DAY)).abs / MILLISECONDS_IN_HOUR)
95
+ end
96
+
97
+ # The minutes component.
98
+ #
99
+ def minutes
100
+ return sign *
101
+ ((@totalMilliseconds - (days * MILLISECONDS_IN_DAY) - (hours * MILLISECONDS_IN_HOUR)).abs /
102
+ MILLISECONDS_IN_MINUTE)
103
+ end
104
+
105
+ # The seconds component.
106
+ #
107
+ def seconds
108
+ return sign *
109
+ ((@totalMilliseconds - (days * MILLISECONDS_IN_DAY) - (hours * MILLISECONDS_IN_HOUR) -
110
+ (minutes * MILLISECONDS_IN_MINUTE)).abs /
111
+ MILLISECONDS_IN_SECOND)
112
+ end
113
+
114
+ # The milliseconds component.
115
+ #
116
+ def milliseconds
117
+ return @totalMilliseconds -
118
+ (days * MILLISECONDS_IN_DAY) -
119
+ (hours * MILLISECONDS_IN_HOUR) -
120
+ (minutes * MILLISECONDS_IN_MINUTE) -
121
+ (seconds * MILLISECONDS_IN_SECOND)
122
+ end
123
+
124
+ # Get the total number of hours in this time span. For example, if
125
+ # this time span represents two days, this method will return 48.
126
+ #
127
+ # @return This timespan in hours
128
+ #
129
+ def total_hours
130
+ return sign * (@totalMilliseconds.abs / MILLISECONDS_IN_HOUR)
131
+ end
132
+
133
+ #
134
+ # Get the total number of minutes in this time span. For example, if
135
+ # this time span represents two hours, this method will return 120.
136
+ #
137
+ # @return This timespan in minutes
138
+ #
139
+ def total_minutes
140
+ return sign * (@totalMilliseconds.abs / MILLISECONDS_IN_MINUTE)
141
+ end
142
+
143
+ #
144
+ # Get the total number of seconds in this time span. For example, if
145
+ # this time span represents three minutes, this method will return 180.
146
+ #
147
+ # @return This timespan in seconds
148
+ #
149
+ def total_seconds
150
+ return sign * (@totalMilliseconds.abs / MILLISECONDS_IN_SECOND)
151
+ end
152
+
153
+ #
154
+ # Get the total number of milliseconds in this time span. For example, if
155
+ # this time span represents 4 seconds, this method will return 4000.
156
+ #
157
+ # @return This timespan in milliseconds
158
+ #
159
+ def total_milliseconds
160
+ return @totalMilliseconds
161
+ end
162
+
163
+ #
164
+ # Returns an new SdlTimeSpan instance that is the opposite of this
165
+ # instance
166
+ #
167
+ # @return An instance representing the inverse of this instance
168
+ #
169
+ def negate
170
+ SdlTimeSpan.new(-@totalMilliseconds)
171
+ end
172
+
173
+ #
174
+ # Return a new instance with the days adjusted by the given amount.
175
+ # Positive numbers add days. Negative numbers remove days.
176
+ #
177
+ # @param days The adjustment (days to add or subtract)
178
+ # @return A new instance with the given adjustment.
179
+ #
180
+ def roll_days(days)
181
+ SdlTimeSpan.new(@totalMilliseconds + (days * MILLISECONDS_IN_DAY))
182
+ end
183
+
184
+ #
185
+ # Return a new instance with the hours adjusted by the given amount.
186
+ # Positive numbers add hours. Negative numbers remove hours.
187
+ #
188
+ # @param hours The adjustment (hours to add or subtract)
189
+ # @return A new instance with the given adjustment.
190
+ #
191
+ def roll_hours(hours)
192
+ SdlTimeSpan.new(@totalMilliseconds + (hours * MILLISECONDS_IN_HOUR))
193
+ end
194
+
195
+ #
196
+ # Return a new instance with the minutes adjusted by the given amount.
197
+ # Positive numbers add minutes. Negative numbers remove minutes.
198
+ #
199
+ # @param minutes The adjustment (minutes to add or subtract)
200
+ # @return A new instance with the given adjustment.
201
+ #
202
+ def roll_minutes(minutes)
203
+ SdlTimeSpan.new(@totalMilliseconds + (minutes * MILLISECONDS_IN_MINUTE))
204
+ end
205
+
206
+ #
207
+ # Return a new instance with the seconds adjusted by the given amount.
208
+ # Positive numbers add seconds. Negative numbers remove seconds.
209
+ #
210
+ # @param seconds The adjustment (seconds to add or subtract)
211
+ # @return A new instance with the given adjustment.
212
+ #
213
+ def roll_seconds(seconds)
214
+ SdlTimeSpan.new(@totalMilliseconds + (seconds * MILLISECONDS_IN_SECOND))
215
+ end
216
+
217
+ #
218
+ # Return a new instance with the milliseconds adjusted by the given amount.
219
+ # Positive numbers add milliseconds. Negative numbers remove milliseconds.
220
+ #
221
+ # @param milliseconds The adjustment (milliseconds to add or subtract)
222
+ # @return A new instance with the given adjustment.
223
+ #
224
+ def roll_milliseconds(milliseconds)
225
+ SdlTimeSpan.new(@totalMilliseconds + milliseconds)
226
+ end
227
+
228
+ #
229
+ # A hashcode based on the canonical string representation.
230
+ #
231
+ # @return A hashcode for this time span
232
+ #
233
+ def hash
234
+ to_s.hash
235
+ end
236
+
237
+ # Tests for equivalence.
238
+ #
239
+ # @return true If the given object is equivalent
240
+ #
241
+ def eql?(other)
242
+ other.is_a?(SdlTimeSpan) and @totalMilliseconds == other.total_milliseconds
243
+ end
244
+
245
+ # define '==' as 'eql?'
246
+ alias_method :==, :eql?
247
+
248
+ def <=>(other)
249
+ @totalMilliseconds <=> other.total_milliseconds
250
+ end
251
+
252
+ # <p>Returns an SDL representation of this time span using the format:<p>
253
+ #
254
+ # <pre>
255
+ # (days:)hours:minutes:seconds(.milliseconds)
256
+ # </pre>
257
+ #
258
+ # <p>(parenthesis indicate optional components)
259
+ #
260
+ # <p>The days and milliseconds components will not be included if they
261
+ # are set to 0. Days must be suffixed with "d" for clarity.</p>
262
+ #
263
+ # <p>Hours, minutes, and seconds will be zero paded to two characters.</p>
264
+ #
265
+ # <p>Examples:</p>
266
+ # <pre>
267
+ # 23:13:00 (12 hours and 13 minutes)
268
+ # 24d:12:13:09.234 (24 days, 12 hours, 13 minutes, 9 seconds,
269
+ # 234 milliseconds)
270
+ # </pre>
271
+ #
272
+ # @return an SDL representation of this time span
273
+ #
274
+ def to_s
275
+ _days = days
276
+ _milliseconds = milliseconds
277
+
278
+ s = nil
279
+ if _days == 0
280
+ if _milliseconds == 0
281
+ s = sprintf("%d:%02d:%02d", hours, minutes.abs, seconds.abs)
282
+ else
283
+ s = sprintf("%d:%02d:%02d.%03d", hours, minutes.abs, seconds.abs, _milliseconds.abs)
284
+ end
285
+ else
286
+ if _milliseconds == 0
287
+ s = sprintf("%dd:%02d:%02d:%02d", _days, hours.abs, minutes.abs, seconds.abs)
288
+ else
289
+ s = sprintf(
290
+ "%dd:%02d:%02d:%02d.%03d",
291
+ _days,
292
+ hours.abs,
293
+ minutes.abs,
294
+ seconds.abs,
295
+ _milliseconds.abs)
296
+ end
297
+ end
298
+ return s
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,949 @@
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 'stringio'
20
+
21
+ require File.dirname(__FILE__) + '/sdl'
22
+ require File.dirname(__FILE__) + '/parser'
23
+
24
+ # SDL (Simple Declarative Language) documents are made up of Tags. A Tag
25
+ # contains
26
+ #
27
+ # * a name (if not present, the name "content" is used)
28
+ # * a namespace (optional)
29
+ # * 0 or more values (optional)
30
+ # * 0 or more attributes (optional)
31
+ # * 0 or more children (optional)
32
+ #
33
+ # For the SDL code:
34
+ #
35
+ # size 4
36
+ # smoker false
37
+ #
38
+ #
39
+ # Assuming this code is in a file called values.sdl, the values can be read
40
+ # using the following code (ignoring exceptions):
41
+ #
42
+ # root = Tag.new("root").read(new File("values.sdl"));
43
+ # int size = root.getChild("size").intValue();
44
+ # boolean smoker = root.getChild("smoker").booleanValue();
45
+ #
46
+ # A tag is basically a data structure with a list of values, a map of
47
+ # attributes, and (if it has a body) child tags. In the example above, the
48
+ # "values.sdl" file is read into a tag called "root". It has two children
49
+ # (tags) called "size" and "smoker". Both these children have one value, no
50
+ # attributes, and no bodies.
51
+ #
52
+ # SDL is often used for simple key-value mappings. To simplify things Tag
53
+ # has the methods getValue and setValue which operate on the first element in
54
+ # the values list. Also notice SDL understands types which are determined
55
+ # using type inference.
56
+ #
57
+ # The example above used the simple format common in property files:
58
+ #
59
+ # name value
60
+ #
61
+ # The full SDL tag format is:
62
+ #
63
+ # namespace:name value_list attribute_list {
64
+ # children_tags
65
+ # }
66
+ #
67
+ # where value_list is zero or more space separated SDL literals and
68
+ # attribute_list is zero or more space separated (namespace:)key=value pairs.
69
+ # The name, namespace, and keys are SDL identifiers. Values are SDL literals.
70
+ # Namespace is optional for both tag names and attributes. Tag bodies are also
71
+ # optional. SDL identifiers begin with a unicode letter or an underscore (_)
72
+ # followed by zero or more unicode letters, numbers, underscores (_) and
73
+ # dashes (-).
74
+ #
75
+ # SDL also supports anonymous tags which are assigned the name "content".
76
+ # An annoymous tag starts with a literal and is followed by zero or more
77
+ # additional literals and zero or more attributes. The examples section below
78
+ # demonstrates the use of anonymous tags.
79
+ #
80
+ # Tags without bodies are terminated by a new line character (\n) and may be
81
+ # continue onto the next line by placing a backslash (\) at the end of the
82
+ # line. Tags may be nested to an arbitrary depth. SDL ignores all other white
83
+ # space characters between tokens. Although nested blocks are indented by
84
+ # convention, tabs have no significance in the language.
85
+ #
86
+ # There are two ways to write String literals.
87
+ #
88
+ # 1. Starting and ending with double quotes ("). Double quotes, backslash
89
+ # characters (\), and new lines (\n) within this type of String literal
90
+ # must be escaped like so:
91
+ #
92
+ # file "C:\\folder\\file.txt"
93
+ # say "I said \"something\""
94
+ #
95
+ # This type of String literal can be continued on the next line by placing a
96
+ # backslash (\) at the end of the line like so:
97
+ #
98
+ # line "this is a \
99
+ # long string of text"
100
+ #
101
+ # White space before the first character in the second line will be ignored.
102
+ #
103
+ # 2. Starting and ending with a backquote (`). This type of string literal
104
+ # can only be ended with a second backquote (`). It is not necessary (or
105
+ # possible) to escape any type of character within a backquote string
106
+ # literal. This type of literal can also span lines. All white spaces are
107
+ # preserved including new lines.
108
+ #
109
+ # Examples:
110
+ #
111
+ # file `C:\folder\file.txt`
112
+ # say `I said "something"`
113
+ # regex `\w+\.suite\(\)`
114
+ # long_line `This is
115
+ # a long line
116
+ # fee fi fo fum`
117
+ #
118
+ # Note: SDL interprets new lines in `` String literals as a single new line
119
+ # character (\n) regarless of the platform.
120
+ #
121
+ # Binary literals use base64 characters enclosed in square brackets ([]).
122
+ # The binary literal type can also span lines. White space is ignored.
123
+ #
124
+ #
125
+ # Examples:
126
+ # key [sdf789GSfsb2+3324sf2] name="my key"
127
+ # image [
128
+ # R3df789GSfsb2edfSFSDF
129
+ # uikuikk2349GSfsb2edfS
130
+ # vFSDFR3df789GSfsb2edf
131
+ # ]
132
+ # upload from="ikayzo.com" data=[
133
+ # R3df789GSfsb2edfSFSDF
134
+ # uikuikk2349GSfsb2edfS
135
+ # vFSDFR3df789GSfsb2edf
136
+ # ]
137
+ #
138
+ # SDL supports date, time span, and date/time literals. Date and Date/Time
139
+ # literals use a 24 hour clock (0-23). If a timezone is not specified, the
140
+ # default locale's timezone will be used.
141
+ #
142
+ # Examples:
143
+ #
144
+ # # create a tag called "date" with a date value of Dec 5, 2005
145
+ # date 2005/12/05
146
+ #
147
+ # # various time span literals
148
+ # hours 03:00:00
149
+ # minutes 00:12:00
150
+ # seconds 00:00:42
151
+ # short_time 00:12:32.423 # 12 minutes, 32 seconds, 423 milliseconds
152
+ # long_time 30d:15:23:04.023 # 30 days, 15 hours, 23 mins, 4 secs, 23 millis
153
+ # before -00:02:30 # 2 hours and 30 minutes ago
154
+ #
155
+ # # a date time literal
156
+ # in_japan 2005/12/05 14:12:23.345-JST
157
+ #
158
+ #
159
+ # SDL 1.0 has thirteen literal types (parenthesis indicate optional components)
160
+ #
161
+ # 1. string (unicode) - examples: "hello" or `aloha`
162
+ # 2. character (unicode) - example: '/'
163
+ # Note: &#92;uXXXX style unicode escapes are not supported (or
164
+ # needed because sdl files are UTF8)
165
+ # 3. integer (32 bits signed) - example: 123
166
+ # 4. long integer (64 bits signed) - examples: 123L or 123l
167
+ # 5. float (32 bits signed) - examples 123.43F 123.43f
168
+ # 6. double float (64 bits signed) - example: 123.43 or 123.43d or 123.43D
169
+ # 7. decimal (128+ bits signed) - example: 123.44BD or 123.44bd
170
+ # 8. boolean - examples: true or false or on or off
171
+ # 9. date yyyy/mm/dd - example 2005/12/05
172
+ # 10. date time yyyy/mm/dd hh:mm(:ss)(.xxx)(-ZONE)
173
+ # example - 2005/12/05 05:21:23.532-JST
174
+ # notes: uses a 24 hour clock (0-23), only hours and minutes are
175
+ # mandatory
176
+ # 11. time span using the format (d:)hh:mm:ss(.xxx)
177
+ # notes: if the day component is included it must be suffixed with
178
+ # a lower case 'd'
179
+ # examples 12:14:42 (12 hours, 14 minutes, 42 seconds)
180
+ # 00:09:12 (9 minutes, 12 seconds)
181
+ # 00:00:01.023 (1 second, 23 milliseconds)
182
+ # 23d:05:21:23.532 (23 days, 5 hours, 21 minutes,
183
+ # 23 seconds, 532 milliseconds)
184
+ # 12. binary [base64] exmaple - [sdf789GSfsb2+3324sf2]
185
+ # 13. null
186
+ #
187
+ #
188
+ # Timezones must be specified using a valid time zone ID (ex. America/Los_Angeles), three letter
189
+ # abbreviation (ex. HST), or GMT(+/-)hh(:mm) formatted custom timezone (ex. GMT+02 or GMT+02:30)
190
+ #
191
+ # Note: SDL 1.1 will likely add a reference literal type.
192
+ #
193
+ # These types are designed to be portable across Java, .NET, and other
194
+ # popular platforms.
195
+ #
196
+ #
197
+ # SDL supports four comment types.
198
+ #
199
+ # 1. // single line comments identicle to those used in Java, C, etc. // style
200
+ # comments can occur anywhere in a line. All text after // up to the new line
201
+ # will be ignored.
202
+ # 2. # property style comments. They work the same way as //
203
+ # 3. -- separator comments useful for visually dividing content. They work the same way as //
204
+ # 4. Slash star (/*) style multiline comments. These begin with a slash
205
+ # star and end with a star slash. Everything in between is ignored.
206
+ #
207
+ #
208
+ #
209
+ # An example SDL file:
210
+ #
211
+ # # a tag having only a name
212
+ # my_tag
213
+ #
214
+ # # three tags acting as name value pairs
215
+ # first_name "Akiko"
216
+ # last_name "Johnson"
217
+ # height 68
218
+ #
219
+ # # a tag with a value list
220
+ # person "Akiko" "Johnson" 68
221
+ #
222
+ # # a tag with attributes
223
+ # person first_name="Akiko" last_name="Johnson" height=68
224
+ #
225
+ # # a tag with values and attributes
226
+ # person "Akiko" "Johnson" height=60
227
+ #
228
+ # # a tag with attributes using namespaces
229
+ # person name:first-name="Akiko" name:last-name="Johnson"
230
+ #
231
+ # # a tag with values, attributes, namespaces, and children
232
+ # my_namespace:person "Akiko" "Johnson" dimensions:height=68 {
233
+ # son "Nouhiro" "Johnson"
234
+ # daughter "Sabrina" "Johnson" location="Italy" {
235
+ # hobbies "swimming" "surfing"
236
+ # languages "English" "Italian"
237
+ # smoker false
238
+ # }
239
+ # }
240
+ #
241
+ # ------------------------------------------------------------------
242
+ # // (notice the separator style comment above...)
243
+ #
244
+ # # a log entry
245
+ # # note - this tag has two values (date_time and string) and an
246
+ # # attribute (error)
247
+ # entry 2005/11/23 10:14:23.253-GMT "Something bad happened" error=true
248
+ #
249
+ # # a long line
250
+ # mylist "something" "another" true "shoe" 2002/12/13 "rock" \
251
+ # "morestuff" "sink" "penny" 12:15:23.425
252
+ #
253
+ # # a long string
254
+ # text "this is a long rambling line of text with a continuation \
255
+ # and it keeps going and going..."
256
+ #
257
+ # # anonymous tag examples
258
+ #
259
+ # files {
260
+ # "/folder1/file.txt"
261
+ # "/file2.txt"
262
+ # }
263
+ #
264
+ # # To retrieve the files as a list of strings
265
+ # #
266
+ # # List files = tag.getChild("files").getChildrenValues("content");
267
+ # #
268
+ # # We us the name "content" because the files tag has two children, each of
269
+ # # which are anonymous tags (values with no name.) These tags are assigned
270
+ # # the name "content"
271
+ #
272
+ # matrix {
273
+ # 1 2 3
274
+ # 4 5 6
275
+ # }
276
+ #
277
+ # # To retrieve the values from the matrix (as a list of lists)
278
+ # #
279
+ # # List rows = tag.getChild("matrix").getChildrenValues("content");
280
+ #
281
+ #
282
+ # Example of getting the "location" attribute from the "daughter" tag
283
+ # above (ignoring exceptions)
284
+ #
285
+ # Tag root = new Tag("root").read("myfile.sdl");
286
+ # Tag daughter = root.getChild("daughter", true); // recursive search
287
+ # String location = daughter.getAttribute("location").toString();
288
+ #
289
+ # SDL is normally stored in a file with the .sdl extension. These files
290
+ # should always be encoded using UTF8. SDL fully supports unicode in
291
+ # identifiers and literals.
292
+ #
293
+ # @author Daniel Leuck
294
+ #
295
+ class Tag
296
+
297
+ #
298
+ # the name of this Tag
299
+ #
300
+ attr_reader :name
301
+
302
+ #
303
+ # the namespace of this Tag or an empty string when there is no namespace.
304
+ #
305
+ attr_reader :namespace
306
+
307
+ # Creates an empty tag in the given namespace. If the +namespace+ is null
308
+ # it will be coerced to an empty String.
309
+ #
310
+ # +namespace+:: the namespace for this tag
311
+ # +name+:: the name of this tag
312
+ #
313
+ # Throws ArgumentError if the name is not a legal SDL identifier
314
+ # (see SDL#validate_identifier) or the namespace is non-blank
315
+ # and is not a legal SDL identifier.
316
+ #
317
+ def initialize(name, namespace = "")
318
+ namespace = namespace.to_s
319
+ SDL4R.validate_identifier(namespace) unless namespace.empty?
320
+ @namespace = namespace
321
+
322
+ name = name.to_s.strip
323
+ raise ArgumentError, "Tag name cannot be null or empty" if name.empty?
324
+ SDL4R.validate_identifier(name)
325
+ @name = name
326
+
327
+ @children = []
328
+ @values = []
329
+
330
+ # a Hash of Hash : {namespace => {name => value}}
331
+ # The default namespace is represented by an empty string.
332
+ @attributesByNamespace = {}
333
+ end
334
+
335
+ # Add a child to this Tag.
336
+ #
337
+ # +child+:: The child to add
338
+ #
339
+ # Returns the added child.
340
+ #
341
+ def add_child(child)
342
+ @children.push(child)
343
+ return child
344
+ end
345
+
346
+ # Adds the given object as a child if it is a +Tag+, as an attribute if it is a Hash
347
+ # {key => value} (supports namespaces), or as a value otherwise.
348
+ #
349
+ # Returns +self+.
350
+ #
351
+ def <<(o)
352
+ if o.is_a?(Tag)
353
+ add_child(o)
354
+ elsif o.is_a?(Hash)
355
+ o.each_pair { |key, value|
356
+ namespace, key = key.split(/:/) if key.match(/:/)
357
+ namespace ||= ""
358
+ set_attribute(key, value, namespace)
359
+ }
360
+ else
361
+ add_value(o)
362
+ end
363
+ return self
364
+ end
365
+
366
+ # Remove a child from this Tag
367
+ #
368
+ # +child+:: the child to remove
369
+ #
370
+ # Returns true if the child exists and is removed
371
+ #
372
+ def remove_child(child)
373
+ return !@children.delete(child).nil?
374
+ end
375
+
376
+ # Removes all children.
377
+ #
378
+ def clear_children
379
+ @children = []
380
+ end
381
+
382
+ #
383
+ # A convenience method that sets the first value in the value list. See
384
+ # {@link #addValue(Object)} for legal types.
385
+ #
386
+ # +value+:: The value to be set.
387
+ # @throws IllegalArgumentException if the value is not a legal SDL type
388
+ #
389
+ def value=(value)
390
+ @values[0] = SDL4R.coerce_or_fail(value)
391
+ end
392
+
393
+ #
394
+ # A convenience method that returns the first value.
395
+ #
396
+ def value
397
+ @values[0]
398
+ end
399
+
400
+ # Returns the number of children Tag.
401
+ #
402
+ def child_count
403
+ @children.size
404
+ end
405
+
406
+ # Returns an Array of the children Tags of this Tag.
407
+ #
408
+ # +recursive+:: if true children and all descendants will be returned. False by default.
409
+ # +name+:: if not nil, only children having this name will be returned. Nil by default.
410
+ # +namespace+:: if not nil, only children having this namespace will be returned.
411
+ # Nil by default.
412
+ #
413
+ def children(recursive = false, name = nil, namespace = nil, &block)
414
+ if block_given?
415
+ each_child(recursive, name, namespace, &block)
416
+
417
+ else
418
+ unless recursive or name or namespace
419
+ @children
420
+
421
+ else
422
+ result = []
423
+ each_child(recursive, name, namespace) { |child|
424
+ result << child
425
+ }
426
+ return result
427
+ end
428
+ end
429
+ end
430
+
431
+ # Returns the values of all the children with the given +name+. If the child has
432
+ # more than one value, all the values will be added as a list. If the child
433
+ # has no value, +nil+ will be added. The search is not recursive.
434
+ #
435
+ # +name+:: if nil, all children are considered (nil by default).
436
+ def children_values(name = nil)
437
+ children_values = []
438
+ each_child(false, name) { |child|
439
+ case child.values.size
440
+ when 0
441
+ children_values << nil
442
+ when 1
443
+ children_values << child.value
444
+ else
445
+ children_values << child.values
446
+ end
447
+ }
448
+ return children_values
449
+ end
450
+
451
+ # Get the first child with the given name, optionally using a recursive search.
452
+ #
453
+ # +name+:: the name of the child Tag. If +nil+, the first child is returned (+nil+ if there are
454
+ # no children at all).
455
+ #
456
+ # Returns The first child tag having the given name or +nil+ if no such child exists
457
+ #
458
+ def child(name = nil, recursive = false)
459
+ unless name
460
+ return @children.first
461
+ else
462
+ each_child(recursive, name) { |child| return child }
463
+ end
464
+ end
465
+
466
+ # Enumerates the children +Tag+s of this Tag and calls the given block
467
+ # providing it the child as parameter.
468
+ #
469
+ # +recursive+:: if true, enumerate grand-children, etc, recursively
470
+ # +name+:: if not nil, indicates the name of the children to enumerate
471
+ # +namespace+:: if not nil, indicates the namespace of the children to enumerate
472
+ #
473
+ def each_child(recursive = false, name = nil, namespace = nil, &block)
474
+ @children.each do |child|
475
+ if (name.nil? or child.name == name) and
476
+ (namespace.nil? or child.namespace == namespace)
477
+ yield child
478
+ end
479
+
480
+ child.children(recursive, name, namespace, &block) if recursive
481
+ end
482
+ return nil
483
+ end
484
+ private :each_child
485
+
486
+ # Adds a value to this Tag. The allowable types are String, Number,
487
+ # Boolean, Character, byte[], Byte[] (coerced to byte[]), Calendar,
488
+ # Date (coerced to Calendar), and null. Passing any other type will
489
+ # result in an IllegalArgumentException.
490
+ #
491
+ # +v+:: The value to add
492
+ #
493
+ # Raises a +ArgumentError+ if the value is not a legal SDL type
494
+ #
495
+ def add_value(v)
496
+ @values.push(SDL4R::coerce_or_fail(v))
497
+ end
498
+
499
+ # Returns true if +v+ is a value of this Tag's.
500
+ #
501
+ def has_value(v)
502
+ @values.include?(v)
503
+ end
504
+
505
+ # Remove a value from this Tag.
506
+ #
507
+ # +v+:: The value to remove
508
+ #
509
+ # Returns true If the value exists and is removed
510
+ #
511
+ def remove_value(v)
512
+ return !@values.delete(v).nil?
513
+ end
514
+
515
+ # Removes all values.
516
+ #
517
+ def clear_values
518
+ @values = []
519
+ end
520
+
521
+ # Returns an Array of the values of this Tag
522
+ def values
523
+ if block_given?
524
+ @values.each { |v| yield v }
525
+ else
526
+ return @values
527
+ end
528
+ end
529
+
530
+ #
531
+ # Set the values for this tag. See {@link #addValue(Object)} for legal
532
+ # value types.
533
+ #
534
+ # +values+:: The new values
535
+ # @throws IllegalArgumentException if the collection contains any values
536
+ # which are not legal SDL types
537
+ #
538
+ def values=(someValues)
539
+ @values.clear()
540
+ someValues.to_a.each { |v|
541
+ # this is required to ensure validation of types
542
+ add_value(v)
543
+ }
544
+ end
545
+
546
+ #
547
+ # Set an attribute in the given namespace for this tag. The allowable
548
+ # attribute value types are the same as those allowed for
549
+ # {@link #addValue(Object)}
550
+ #
551
+ # +namespace+:: The namespace for this attribute
552
+ # +key+:: The attribute key
553
+ # +value+:: The attribute value
554
+ # @throws IllegalArgumentException if the key is not a legal SDL
555
+ # identifier (see {@link SDL#validateIdentifier(String)}), or the
556
+ # namespace is non-blank and is not a legal SDL identifier, or the
557
+ # value is not a legal SDL type
558
+ #
559
+ def set_attribute(key, value, namespace = "")
560
+ SDL4R.validate_identifier(namespace) unless namespace.empty?
561
+ SDL4R.validate_identifier(key)
562
+
563
+ attributes = @attributesByNamespace[namespace]
564
+
565
+ if attributes.nil?
566
+ attributes = {}
567
+ @attributesByNamespace[namespace] = attributes
568
+ end
569
+
570
+ attributes[key] = SDL4R.coerce_or_fail(value)
571
+ end
572
+
573
+ # Returns the attribute of the specified +namespace+ of specified +key+ or +nil+ if not found.
574
+ #
575
+ # +namespace+:: the default namespace ("") by default
576
+ #
577
+ def attribute(key, namespace = "")
578
+ attributes = @attributesByNamespace[namespace]
579
+ return attributes.nil? ? nil : attributes[key]
580
+ end
581
+
582
+ # Indicates whether the specified attribute exists in this Tag.
583
+ #
584
+ # +key+:: key of the attribute
585
+ # +namespace+:: namespace of the attribute ("", the default namespace, by default)
586
+ #
587
+ def has_attribute(key, namespace = "")
588
+ attributes = @attributesByNamespace[namespace]
589
+ return attributes.nil? ? false : attributes.has_key?(key)
590
+ end
591
+
592
+ # Returns a copy of the Hash of the attributes of the specified +namespace+ (default is all).
593
+ #
594
+ # +namespace+:: namespace of the returned attributes. If nil, all attributes are returned with
595
+ # qualified names (e.g. "meat:color"). If "" attributes of the default namespace are returned.
596
+ #
597
+ def attributes(namespace = nil, &block)
598
+ if block_given?
599
+ each_attribute(namespace, &block)
600
+
601
+ else
602
+ if namespace.nil?
603
+ hash = {}
604
+
605
+ each_attribute(nil) do | key, value, namespace |
606
+ qualified_name = namespace.empty? ? key : namespace + ':' + key
607
+ hash[qualified_name] = value
608
+ end
609
+
610
+ return hash
611
+
612
+ else
613
+ hash = @attributesByNamespace[namespace]
614
+ return hash.clone
615
+ end
616
+ end
617
+ end
618
+
619
+ # Removes the attribute, whose name and namespace are specified.
620
+ #
621
+ # +key+:: name of the removed atribute
622
+ # +namespace+:: namespace of the removed attribute (equal to "", default namespace, by default)
623
+ #
624
+ # Returns the value of the removed attribute or +nil+ if it didn't exist.
625
+ #
626
+ def remove_attribute(key, namespace = "")
627
+ attributes = @attributesByNamespace[namespace]
628
+ return attributes.nil? ? nil : attributes.delete(key)
629
+ end
630
+
631
+ # Clears the attributes of the specified namespace or all the attributes if +namespace+ is
632
+ # +nil+.
633
+ #
634
+ def clear_attributes(namespace = nil)
635
+ if namespace.nil?
636
+ @attributesByNamespace.clear
637
+ else
638
+ @attributesByNamespace.delete(namespace)
639
+ end
640
+ end
641
+
642
+ # Enumerates the attributes for the specified +namespace+.
643
+ # If no +namespace+ is specified, enumerates the attribute of the default
644
+ # namespace. If +namespace+ is nil, enumerates the attributes of all
645
+ # namespaces.
646
+ #
647
+ def each_attribute(namespace = "", &block)
648
+ if namespace.nil?
649
+ @attributesByNamespace.each_key { |a_namespace| each_attribute(a_namespace, &block) }
650
+
651
+ else
652
+ attributes = @attributesByNamespace[namespace]
653
+ unless attributes.nil?
654
+ attributes.each_pair do |key, value|
655
+ yield key, value, namespace
656
+ end
657
+ end
658
+ end
659
+ end
660
+ private :each_attribute
661
+
662
+ # Sets all the attributes of a +namespace+ for this Tag in one operation.
663
+ # See # #add_value for allowable attribute value types.
664
+ #
665
+ # +attributes+:: a Hash where keys are attribute keys
666
+ # +namespace+:: "" (default namespace) by default
667
+ #
668
+ # Raises an +ArgumentError+ if any key in the map is not a legal SDL
669
+ # identifier (see SDL#validate_identifier), or any value
670
+ # is not a legal SDL type.
671
+ #
672
+ def set_attributes(attribute_hash, namespace = "")
673
+ return if attribute_hash.nil? or attribute_hash.empty?
674
+
675
+ attributes = @attributesByNamespace[namespace]
676
+ if attributes.nil?
677
+ attributes = {}
678
+ @attributesByNamespace[namespace] = attributes
679
+ else
680
+ attributes.clear()
681
+ end
682
+
683
+ attribute_hash.each_pair do |key, value|
684
+ # Calling set_attribute() is required to ensure validations
685
+ set_attribute(key, value)
686
+ end
687
+ end
688
+
689
+ # Sets all the attributes of the default namespace for this Tag in one
690
+ # operation.
691
+ #
692
+ # See #set_attributes
693
+ #
694
+ def attributes=(attribute_hash)
695
+ set_attributes(attribute_hash, "")
696
+ end
697
+
698
+ # Sets the name of this Tag.
699
+ #
700
+ # Raises +ArgumentError+ if the name is not a legal SDL
701
+ # identifier (see SDL#validate_identifier)
702
+
703
+ def name=(a_name)
704
+ a_name = a_name.to_s
705
+ SDL4R.validate_identifier(a_name)
706
+ @name = a_name
707
+ end
708
+
709
+ # The namespace to set. null will be coerced to the empty string.
710
+ #
711
+ # Raises +ArgumentError+ if the namespace is non-blank and is not
712
+ # a legal SDL identifier (see {@link SDL#validate_identifier(String)})
713
+
714
+ def namespace=(a_namespace)
715
+ a_namespace = a_namespace.to_s
716
+ SDL4R.validate_identifier(a_namespace) unless a_namespace.empty?
717
+ @namespace = a_namespace
718
+ end
719
+
720
+ #
721
+ # Add all the tags specified in the file at the given URL to this Tag.
722
+ #
723
+ # +url+:: A UTF8 encoded .sdl file
724
+ # @throws IOException If there is an IO problem reading the source
725
+ # @throws ParseException If the SDL input is malformed
726
+ # Returns This tag after adding all the children read from the reader
727
+ #
728
+ # public Tag read(URL url) throws IOException, SDLParseException {
729
+ # return read(new InputStreamReader(url.openStream(), "UTF8"));
730
+ # }
731
+
732
+ #
733
+ # Add all the tags specified in the given file to this Tag.
734
+ #
735
+ # +file+:: A UTF8 encoded .sdl file
736
+ # @throws IOException If there is an IO problem reading the source
737
+ # @throws ParseException If the SDL input is malformed
738
+ # Returns This tag after adding all the children read from the reader
739
+ #
740
+ # public Tag read(File file) throws IOException, SDLParseException {
741
+ # return read(new InputStreamReader(new FileInputStream(file), "UTF8"));
742
+ # }
743
+
744
+ #
745
+ # Add all the tags specified in the given String to this Tag.
746
+ #
747
+ # +text+:: An SDL string
748
+ # @throws ParseException If the SDL input is malformed
749
+ # Returns This tag after adding all the children read from the reader
750
+ #
751
+ # public Tag read(String text) throws SDLParseException {
752
+ # try {
753
+ # return read(new StringReader(text));
754
+ # } catch(IOException ioe) {
755
+ # // Cannot happen
756
+ # throw new InternalError("IOExceptio reading a String");
757
+ # }
758
+ # }
759
+
760
+ # Adds all the tags specified in the given +IO+ or String to this Tag.
761
+ #
762
+ # Returns this Tag after adding all the children read from _input_.
763
+ #
764
+ def read(input)
765
+ if input.is_a?(String)
766
+ io = StringIO.new(input)
767
+ else
768
+ io = input
769
+ end
770
+
771
+ Parser.new(io).parse.each do |tag|
772
+ add_child(tag)
773
+ end
774
+
775
+ return self
776
+ end
777
+
778
+ # Write this tag out to the given IO or string (optionally clipping the root.)
779
+ #
780
+ # +writer+:: The writer to which we will write this tag
781
+ # +includeRoot+:: If true this tag will be written out as the root
782
+ # element, if false only the children will be written
783
+ #
784
+ def write(output, include_root = false)
785
+ io = (output.is_a?(String))? StringIO.new(output) : output
786
+
787
+ if include_root
788
+ io << to_s
789
+ else
790
+ each_child do |child, index|
791
+ io << $/ if index > 0
792
+ child.write(io)
793
+ end
794
+ end
795
+
796
+ io.close()
797
+ end
798
+
799
+ #
800
+ # Get a String representation of this SDL Tag. This method returns a
801
+ # complete description of the Tag's state using SDL (i.e. the output can
802
+ # be parsed by {@link #read(String)})
803
+ #
804
+ # Returns A string representation of this tag using SDL
805
+ #
806
+ def to_s
807
+ to_string
808
+ end
809
+
810
+ #
811
+ # +linePrefix+:: A prefix to insert before every line.
812
+ # Returns A string representation of this tag using SDL
813
+ #
814
+ # TODO: break up long lines using the backslash
815
+ #
816
+ def to_string(line_prefix = "", indent = "\t")
817
+ line_prefix = "" if line_prefix.nil?
818
+ s = ""
819
+ s << line_prefix
820
+
821
+ if name == "content" && namespace.empty?
822
+ skip_value_space = true
823
+ else
824
+ skip_value_space = false
825
+ s << "#{namespace}:" unless namespace.empty?
826
+ s << name
827
+ end
828
+
829
+ # output values
830
+ values do |value|
831
+ if skip_value_space
832
+ skip_value_space = false
833
+ else
834
+ s << " "
835
+ end
836
+ s << SDL4R.format(value, true, line_prefix, indent)
837
+ end
838
+
839
+ # output attributes
840
+ unless @attributesByNamespace.empty?
841
+ all_attributes_hash = attributes(nil)
842
+ all_attributes_array = all_attributes_hash.sort { |a, b|
843
+ namespace1, name1 = a[0].split(':')
844
+ namespace1, name1 = "", namespace1 if name1.nil?
845
+ namespace2, name2 = b[0].split(':')
846
+ namespace2, name2 = "", namespace2 if name2.nil?
847
+
848
+ diff = namespace1 <=> namespace2
849
+ diff == 0 ? name1 <=> name2 : diff
850
+ }
851
+ all_attributes_array.each do |attribute_name, attribute_value|
852
+ s << " " << attribute_name << '=' << SDL4R.format(attribute_value, true)
853
+ end
854
+ end
855
+
856
+ # output children
857
+ unless @children.empty?
858
+ s << " {#{$/}"
859
+ children_to_string(line_prefix + indent, s)
860
+ s << line_prefix << ?}
861
+ end
862
+
863
+ return s
864
+ end
865
+
866
+ # Returns a string representation of the children tags.
867
+ #
868
+ # +linePrefix+:: A prefix to insert before every line.
869
+ # +s+:: a String that receives the string representation
870
+ #
871
+ # TODO: break up long lines using the backslash
872
+ #
873
+ def children_to_string(line_prefix = "", s = "")
874
+ @children.each do |child|
875
+ s << child.to_string(line_prefix) << $/
876
+ end
877
+
878
+ return s
879
+ end
880
+
881
+ # Returns true if this tag (including all of its values, attributes, and
882
+ # children) is equivalent to the given tag.
883
+ #
884
+ # Returns true if the tags are equivalet
885
+ #
886
+ def eql?(o)
887
+ # this is safe because to_string() dumps the full state
888
+ return o.is_a?(Tag) && o.to_string == to_string;
889
+ end
890
+ alias_method :==, :eql?
891
+
892
+ # Returns The hash (based on the output from toString())
893
+ #
894
+ def hash
895
+ return to_string.hash
896
+ end
897
+
898
+ # Returns a string containing an XML representation of this tag. Values
899
+ # will be represented using _val0, _val1, etc.
900
+ #
901
+ # Returns An XML String describing this Tag
902
+ # +linePrefix+:: A prefix to insert before every line.
903
+ # Returns A String containing an XML representation of this tag. Values
904
+ # will be represented using _val0, _val1, etc.
905
+ #
906
+ def to_xml_string(linePrefix = "")
907
+ linePrefix = "" if linePrefix.nil?
908
+
909
+ s = ""
910
+ s << linePrefix << ?<
911
+ s << "#{namespace}:" unless namespace.empty?
912
+ s << name
913
+
914
+ # output values
915
+ unless @values.empty?
916
+ i = 0
917
+ @values.each do |value|
918
+ s << " _val" << i.to_s << "=\"" << SDL4R.format(value, false) << "\""
919
+ i += 1
920
+ end
921
+ end
922
+
923
+ # output attributes
924
+ unless @attributes.empty?
925
+ @attributes.each do |attribute_name, attribute_value|
926
+ s << " "
927
+ attribute_namespace = @attributeToNamespace[attribute_name]
928
+ s << "#{attribute_namespace}:" unless attribute_namespace.empty?
929
+ s << attribute_name << "=\"" << SDL4R.format(attribute_value, false) << ?"
930
+ end
931
+ end
932
+
933
+ if @children.empty?
934
+ s << "/>"
935
+ else
936
+ s << ">\n"
937
+ @children.each do |child|
938
+ s << child.to_xml_string(linePrefix + " ") << ?\n
939
+ end
940
+
941
+ s << linePrefix << "</"
942
+ s << "#{namespace}:" unless namespace.empty?
943
+ s << name << ?>
944
+ end
945
+
946
+ return s
947
+ end
948
+ end
949
+ end