sxp 0.0.13 → 0.0.14
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +0 -1
- data/CREDITS +1 -0
- data/README +20 -0
- data/VERSION +1 -1
- data/lib/sxp/extensions.rb +11 -0
- data/lib/sxp/reader/sparql.rb +198 -16
- data/lib/sxp/version.rb +1 -1
- data/lib/sxp/writer.rb +74 -0
- metadata +36 -13
data/AUTHORS
CHANGED
data/CREDITS
CHANGED
data/README
CHANGED
@@ -45,6 +45,8 @@ Examples
|
|
45
45
|
|
46
46
|
### Parsing SPARQL S-expressions
|
47
47
|
|
48
|
+
require 'rdf'
|
49
|
+
|
48
50
|
SXP::Reader::SPARQL.read %q((base <http://ar.to/>)) #=> [:base, RDF::URI('http://ar.to/')]
|
49
51
|
|
50
52
|
Documentation
|
@@ -52,6 +54,23 @@ Documentation
|
|
52
54
|
|
53
55
|
* <http://sxp.rubyforge.org/>
|
54
56
|
|
57
|
+
* {SXP}
|
58
|
+
|
59
|
+
### Parsing SXP
|
60
|
+
* {SXP::Reader}
|
61
|
+
* {SXP::Reader::Basic}
|
62
|
+
* {SXP::Reader::CommonLisp}
|
63
|
+
* {SXP::Reader::Extended}
|
64
|
+
* {SXP::Reader::Scheme}
|
65
|
+
* {SXP::Reader::SPARQL}
|
66
|
+
|
67
|
+
### Manipulating SXP
|
68
|
+
* {SXP::Pair}
|
69
|
+
* {SXP::List}
|
70
|
+
|
71
|
+
### Generating SXP
|
72
|
+
* {SXP::Generator}
|
73
|
+
|
55
74
|
Dependencies
|
56
75
|
------------
|
57
76
|
|
@@ -98,6 +117,7 @@ Contributors
|
|
98
117
|
------------
|
99
118
|
|
100
119
|
* [Ben Lavender](https://github.com/bhuga) - <http://bhuga.net/>
|
120
|
+
* [Gregg Kellogg](http://github.com/gkellogg) - <http://kellogg-assoc.com/>
|
101
121
|
|
102
122
|
License
|
103
123
|
-------
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.14
|
data/lib/sxp/extensions.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'rdf'
|
2
|
+
|
1
3
|
##
|
2
4
|
# Extensions for Ruby's `Symbol` class.
|
3
5
|
class Symbol
|
@@ -9,3 +11,12 @@ class Symbol
|
|
9
11
|
to_s[-1] == ?:
|
10
12
|
end
|
11
13
|
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Extensions for RDF::URI
|
17
|
+
class RDF::URI
|
18
|
+
# Original lexical value of this URI to allow for round-trip serialization.
|
19
|
+
def lexical=(value); @lexical = value; end
|
20
|
+
def lexical; @lexical; end
|
21
|
+
end
|
22
|
+
|
data/lib/sxp/reader/sparql.rb
CHANGED
@@ -8,45 +8,171 @@ module SXP; class Reader
|
|
8
8
|
#
|
9
9
|
# @see http://openjena.org/wiki/SSE
|
10
10
|
class SPARQL < Extended
|
11
|
+
# Alias for rdf:type
|
12
|
+
A = /^a$/
|
13
|
+
# Base token, causes next URI to be treated as the `base_uri` for further URI expansion
|
14
|
+
BASE = /^base$/i
|
15
|
+
# Prefix token, causes following prefix and URI pairs to be used for transforming
|
16
|
+
# {PNAME} tokens into URIs.
|
17
|
+
PREFIX = /^prefix$/i
|
11
18
|
NIL = /^nil$/i
|
12
19
|
FALSE = /^false$/i
|
13
20
|
TRUE = /^true$/i
|
14
21
|
EXPONENT = /[eE][+-]?[0-9]+/
|
15
|
-
DECIMAL = /^[+-]?(\d*)?\.\d
|
22
|
+
DECIMAL = /^[+-]?(\d*)?\.\d*$/
|
23
|
+
DOUBLE = /^[+-]?(\d*)?\.\d*#{EXPONENT}$/
|
24
|
+
# BNode with identifier
|
16
25
|
BNODE_ID = /^_:([A-Za-z][A-Za-z0-9]*)/ # FIXME
|
26
|
+
# Anonymous BNode
|
17
27
|
BNODE_NEW = /^_:$/
|
18
|
-
|
19
|
-
|
20
|
-
|
28
|
+
# Distinguished variable with an optional name
|
29
|
+
VAR_ID = /^\?([A-Za-z][A-Za-z0-9]*)?/ # FIXME
|
30
|
+
# Non-distinguished variable with an optional identifier
|
31
|
+
ND_VAR = /^\?\?([0-9]*)/
|
32
|
+
# A URI reference, subject to expansion using `base_uri`
|
21
33
|
URIREF = /^<([^>]+)>/
|
34
|
+
# A QName, subject to expansion to URIs using {PREFIX}
|
35
|
+
PNAME = /([^:]*):([^:]*)/
|
36
|
+
|
37
|
+
RDF_TYPE = (a = RDF.type.dup; a.lexical = 'a'; a).freeze
|
22
38
|
|
23
39
|
##
|
40
|
+
# Base URI as specified or when parsing parsing a BASE token using the immediately following
|
41
|
+
# token, which must be a URI.
|
42
|
+
attr_accessor :base_uri
|
43
|
+
|
44
|
+
##
|
45
|
+
# Prefixes defined while parsing
|
46
|
+
# @return [Hash{Object => RDF::URI}]
|
47
|
+
attr_accessor :prefixes
|
48
|
+
|
49
|
+
##
|
50
|
+
# Defines the given named URI prefix for this parser.
|
51
|
+
#
|
52
|
+
# @example Defining a URI prefix
|
53
|
+
# parser.prefix :dc, RDF::URI('http://purl.org/dc/terms/')
|
54
|
+
#
|
55
|
+
# @example Returning a URI prefix
|
56
|
+
# parser.prefix(:dc) #=> RDF::URI('http://purl.org/dc/terms/')
|
57
|
+
#
|
58
|
+
# @overload prefix(name, uri)
|
59
|
+
# @param [Symbol, #to_s] name
|
60
|
+
# @param [RDF::URI, #to_s] uri
|
61
|
+
#
|
62
|
+
# @overload prefix(name)
|
63
|
+
# @param [Symbol, #to_s] name
|
64
|
+
#
|
65
|
+
# @return [RDF::URI]
|
66
|
+
def prefix(name, uri = nil)
|
67
|
+
name = name.to_s.empty? ? nil : (name.respond_to?(:to_sym) ? name.to_sym : name.to_s.to_sym)
|
68
|
+
uri.nil? ? @prefixes[name] : @prefixes[name] = uri
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Initializes the reader.
|
73
|
+
#
|
74
|
+
# @param [IO, StringIO, String] input
|
75
|
+
# @param [Hash{Symbol => Object}] options
|
76
|
+
def initialize(input, options = {}, &block)
|
77
|
+
super { @prefixes = {}; @bnodes = {}; @list_depth = 0 }
|
78
|
+
|
79
|
+
if block_given?
|
80
|
+
case block.arity
|
81
|
+
when 1 then block.call(self)
|
82
|
+
else self.instance_eval(&block)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Reads SSE Tokens, including {RDF::Literal}, {RDF::URI} and RDF::Node.
|
89
|
+
#
|
90
|
+
# Performs forward reference for prefix and base URI representations and saves in
|
91
|
+
# {#base_uri} and {#prefixes} accessors.
|
92
|
+
#
|
93
|
+
# Transforms tokens matching a {PNAME} pattern into {RDF::URI} instances if a match is
|
94
|
+
# found with a previously identified {PREFIX}.
|
24
95
|
# @return [Object]
|
25
96
|
def read_token
|
26
97
|
case peek_char
|
27
|
-
|
28
|
-
|
29
|
-
|
98
|
+
when ?" then [:atom, read_rdf_literal] # "
|
99
|
+
when ?< then [:atom, read_rdf_uri]
|
100
|
+
else
|
101
|
+
tok = super
|
102
|
+
|
103
|
+
# If we just parsed "PREFIX", and this is an opening list, then
|
104
|
+
# record list depth and process following as token, URI pairs
|
105
|
+
#
|
106
|
+
# Once we've closed the list, go out of prefix mode
|
107
|
+
if tok.is_a?(Array) && tok[0] == :list
|
108
|
+
if '(['.include?(tok[1])
|
109
|
+
@list_depth += 1
|
110
|
+
else
|
111
|
+
@list_depth -= 1
|
112
|
+
@prefix_depth = nil if @prefix_depth && @list_depth < @prefix_depth
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
if tok.is_a?(Array) && tok[0] == :atom && tok[1].is_a?(Symbol)
|
117
|
+
value = tok[1].to_s
|
118
|
+
|
119
|
+
# We previously parsed a PREFIX, this will be the map value
|
120
|
+
@parsed_prefix = value.chop if @prefix_depth && @prefix_depth > 0
|
121
|
+
|
122
|
+
# If we just saw PREFIX, then this starts the parsing mode
|
123
|
+
@prefix_depth = @list_depth + 1 if value =~ PREFIX
|
124
|
+
|
125
|
+
# If the token is of the form 'prefix:suffix', create a URI and give it the
|
126
|
+
# token as a QName
|
127
|
+
if value.to_s =~ PNAME && base = prefix($1)
|
128
|
+
suffix = $2
|
129
|
+
#STDERR.puts "read_tok lexical: pfx: #{$1.inspect} => #{prefix($1).inspect}, sfx: #{suffix.inspect}"
|
130
|
+
suffix = suffix.sub(/^\#/, "") if base.to_s.index("#")
|
131
|
+
uri = RDF::URI(base.to_s + suffix)
|
132
|
+
#STDERR.puts "read_tok lexical uri: #{uri.inspect}"
|
133
|
+
|
134
|
+
# Cause URI to be serialized as a lexical
|
135
|
+
uri.lexical = value
|
136
|
+
[:atom, uri]
|
137
|
+
else
|
138
|
+
tok
|
139
|
+
end
|
140
|
+
else
|
141
|
+
tok
|
142
|
+
end
|
30
143
|
end
|
31
144
|
end
|
32
145
|
|
33
146
|
##
|
147
|
+
# Reads literals corresponding to SPARQL/Turtle/Notation-3 syntax
|
148
|
+
#
|
149
|
+
# @example
|
150
|
+
# "a plain literal"
|
151
|
+
# "a literal with a language"@en
|
152
|
+
# "a typed literal"^^<http://example/>
|
153
|
+
# "a typed literal with a PNAME"^^xsd:string
|
154
|
+
#
|
34
155
|
# @return [RDF::Literal]
|
35
156
|
def read_rdf_literal
|
36
157
|
value = read_string
|
37
158
|
options = case peek_char
|
38
159
|
when ?@
|
39
160
|
skip_char # '@'
|
40
|
-
{:language => read_atom}
|
161
|
+
{:language => read_atom.downcase}
|
41
162
|
when ?^
|
42
163
|
2.times { skip_char } # '^^'
|
43
|
-
{:datatype =>
|
164
|
+
{:datatype => read_token.last}
|
44
165
|
else {}
|
45
166
|
end
|
46
167
|
RDF::Literal(value, options)
|
47
168
|
end
|
48
169
|
|
49
170
|
##
|
171
|
+
# Reads a URI in SPARQL/Turtle/Notation-3 syntax
|
172
|
+
#
|
173
|
+
# @example
|
174
|
+
# <http://example/>
|
175
|
+
#
|
50
176
|
# @return [RDF::URI]
|
51
177
|
def read_rdf_uri
|
52
178
|
buffer = String.new
|
@@ -57,24 +183,54 @@ module SXP; class Reader
|
|
57
183
|
buffer << read_char # TODO: unescaping
|
58
184
|
end
|
59
185
|
skip_char # '>'
|
60
|
-
|
186
|
+
|
187
|
+
# If we have a base URI, use that when constructing a new URI
|
188
|
+
uri = if self.base_uri
|
189
|
+
u = self.base_uri.join(buffer)
|
190
|
+
u.lexical = "<#{buffer}>" unless u.to_s == buffer # So that it can be re-serialized properly
|
191
|
+
u
|
192
|
+
else
|
193
|
+
RDF::URI(buffer)
|
194
|
+
end
|
195
|
+
|
196
|
+
# If we previously parsed a "BASE" element, then this URI is used to set that value
|
197
|
+
if @parsed_base
|
198
|
+
self.base_uri = uri
|
199
|
+
@parsed_base = nil
|
200
|
+
end
|
201
|
+
|
202
|
+
# If we previously parsed a "PREFIX" element, associate this URI with the prefix
|
203
|
+
if @parsed_prefix
|
204
|
+
prefix(@parsed_prefix, uri)
|
205
|
+
@parsed_prefix = nil
|
206
|
+
end
|
207
|
+
|
208
|
+
uri
|
61
209
|
end
|
62
210
|
|
63
211
|
##
|
212
|
+
# Reads an SSE Atom
|
213
|
+
#
|
214
|
+
# Atoms parsed including `base`, `prefix`, `true`, `false`, numeric, BNodes and variables.
|
215
|
+
#
|
216
|
+
# Creates {RDF::Literal}, RDF::Node, or {RDF::Query::Variable} instances where appropriate.
|
217
|
+
#
|
64
218
|
# @return [Object]
|
65
219
|
def read_atom
|
66
220
|
case buffer = read_literal
|
67
221
|
when '.' then buffer.to_sym
|
222
|
+
when A then RDF_TYPE
|
223
|
+
when BASE then @parsed_base = true; buffer.to_sym
|
68
224
|
when NIL then nil
|
69
225
|
when FALSE then RDF::Literal(false)
|
70
226
|
when TRUE then RDF::Literal(true)
|
71
|
-
when
|
72
|
-
when
|
73
|
-
when
|
227
|
+
when DOUBLE then RDF::Literal::Double.new(buffer)
|
228
|
+
when DECIMAL then RDF::Literal::Decimal.new(buffer)
|
229
|
+
when INTEGER then RDF::Literal::Integer.new(buffer)
|
230
|
+
when BNODE_ID then @bnodes[$1] ||= RDF::Node($1)
|
74
231
|
when BNODE_NEW then RDF::Node.new
|
75
|
-
when
|
76
|
-
when
|
77
|
-
when VAR_NEW then RDF::Query::Variable.new
|
232
|
+
when ND_VAR then variable($1, false)
|
233
|
+
when VAR_ID then variable($1, true)
|
78
234
|
else buffer.to_sym
|
79
235
|
end
|
80
236
|
end
|
@@ -91,5 +247,31 @@ module SXP; class Reader
|
|
91
247
|
end
|
92
248
|
end
|
93
249
|
end
|
250
|
+
|
251
|
+
##
|
252
|
+
# Return variable allocated to an ID.
|
253
|
+
# If no ID is provided, a new variable
|
254
|
+
# is allocated. Otherwise, any previous assignment will be used.
|
255
|
+
#
|
256
|
+
# The variable has a #distinguished? method applied depending on if this
|
257
|
+
# is a disinguished or non-distinguished variable. Non-distinguished
|
258
|
+
# variables are effectively the same as BNodes.
|
259
|
+
# @return [RDF::Query::Variable]
|
260
|
+
def variable(id, distinguished = true)
|
261
|
+
id = nil if id.to_s.empty?
|
262
|
+
|
263
|
+
if id
|
264
|
+
@vars ||= {}
|
265
|
+
@vars[id] ||= begin
|
266
|
+
v = RDF::Query::Variable.new(id)
|
267
|
+
v.distinguished = distinguished
|
268
|
+
v
|
269
|
+
end
|
270
|
+
else
|
271
|
+
v = RDF::Query::Variable.new
|
272
|
+
v.distinguished = distinguished
|
273
|
+
v
|
274
|
+
end
|
275
|
+
end
|
94
276
|
end # SPARQL
|
95
277
|
end; end # SXP::Reader
|
data/lib/sxp/version.rb
CHANGED
data/lib/sxp/writer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
1
3
|
##
|
2
4
|
# Extensions for Ruby's `Object` class.
|
3
5
|
class Object
|
@@ -82,6 +84,18 @@ class Integer
|
|
82
84
|
end
|
83
85
|
end
|
84
86
|
|
87
|
+
##
|
88
|
+
# Extensions for Ruby's `BigDecimal` class.
|
89
|
+
class BigDecimal
|
90
|
+
##
|
91
|
+
# Returns the SXP representation of this object.
|
92
|
+
#
|
93
|
+
# @return [String]
|
94
|
+
def to_sxp
|
95
|
+
to_f.to_s
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
85
99
|
##
|
86
100
|
# Extensions for Ruby's `Float` class.
|
87
101
|
class Float
|
@@ -133,3 +147,63 @@ class Regexp
|
|
133
147
|
'#' << inspect
|
134
148
|
end
|
135
149
|
end
|
150
|
+
|
151
|
+
require 'rdf' # For SPARQL
|
152
|
+
|
153
|
+
class RDF::URI
|
154
|
+
##
|
155
|
+
# Returns the SXP representation of this object.
|
156
|
+
#
|
157
|
+
# @return [String]
|
158
|
+
def to_sxp; lexical || "<#{self}>"; end
|
159
|
+
end
|
160
|
+
|
161
|
+
class RDF::Node
|
162
|
+
##
|
163
|
+
# Returns the SXP representation of this object.
|
164
|
+
#
|
165
|
+
# @return [String]
|
166
|
+
def to_sxp; to_s; end
|
167
|
+
end
|
168
|
+
|
169
|
+
class RDF::Literal
|
170
|
+
##
|
171
|
+
# Returns the SXP representation of a Literal.
|
172
|
+
#
|
173
|
+
# @return [String]
|
174
|
+
def to_sxp
|
175
|
+
case datatype
|
176
|
+
when RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.double, RDF::XSD.decimal, RDF::XSD.time
|
177
|
+
object.to_sxp
|
178
|
+
else
|
179
|
+
text = value.dump
|
180
|
+
text << "@#{language}" if self.has_language?
|
181
|
+
text << "^^#{datatype.to_sxp}" if self.has_datatype?
|
182
|
+
text
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
class RDF::Query
|
188
|
+
# Transform Query into an Array form of an SXP
|
189
|
+
#
|
190
|
+
# If Query is named, it's treated as a GroupGraphPattern, otherwise, a BGP
|
191
|
+
#
|
192
|
+
# @return [Array]
|
193
|
+
def to_sxp
|
194
|
+
res = [:bgp] + patterns
|
195
|
+
(respond_to?(:named?) && named? ? [:graph, context, res] : res).to_sxp
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
class RDF::Query::Pattern
|
200
|
+
# Transform Query Pattern into an SXP
|
201
|
+
# @return [String]
|
202
|
+
def to_sxp
|
203
|
+
[:triple, subject, predicate, object].to_sxp
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
class RDF::Query::Variable
|
208
|
+
def to_sxp; to_s; end
|
209
|
+
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 14
|
9
|
+
version: 0.0.14
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Arto Bendiken
|
@@ -14,37 +14,54 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-
|
17
|
+
date: 2011-04-04 00:00:00 +02:00
|
18
18
|
default_executable: sxp2rdf
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
|
-
name:
|
21
|
+
name: json
|
22
22
|
prerelease: false
|
23
23
|
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 5
|
31
|
+
- 1
|
32
|
+
version: 1.5.1
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: yard
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
24
40
|
requirements:
|
25
41
|
- - ">="
|
26
42
|
- !ruby/object:Gem::Version
|
27
43
|
segments:
|
28
44
|
- 0
|
29
45
|
- 6
|
30
|
-
-
|
31
|
-
version: 0.6.
|
46
|
+
- 4
|
47
|
+
version: 0.6.4
|
32
48
|
type: :development
|
33
|
-
version_requirements: *
|
49
|
+
version_requirements: *id002
|
34
50
|
- !ruby/object:Gem::Dependency
|
35
51
|
name: rspec
|
36
52
|
prerelease: false
|
37
|
-
requirement: &
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
38
55
|
requirements:
|
39
56
|
- - ">="
|
40
57
|
- !ruby/object:Gem::Version
|
41
58
|
segments:
|
42
|
-
-
|
43
|
-
-
|
59
|
+
- 2
|
60
|
+
- 5
|
44
61
|
- 0
|
45
|
-
version:
|
62
|
+
version: 2.5.0
|
46
63
|
type: :development
|
47
|
-
version_requirements: *
|
64
|
+
version_requirements: *id003
|
48
65
|
description: A pure-Ruby implementation of a universal S-expression parser.
|
49
66
|
email: arto.bendiken@gmail.com
|
50
67
|
executables:
|
@@ -75,6 +92,10 @@ files:
|
|
75
92
|
- lib/sxp/version.rb
|
76
93
|
- lib/sxp/writer.rb
|
77
94
|
- lib/sxp.rb
|
95
|
+
- bin/sxp2rdf
|
96
|
+
- bin/sxp2json
|
97
|
+
- bin/sxp2xml
|
98
|
+
- bin/sxp2yaml
|
78
99
|
has_rdoc: false
|
79
100
|
homepage: http://sxp.rubyforge.org/
|
80
101
|
licenses:
|
@@ -85,6 +106,7 @@ rdoc_options: []
|
|
85
106
|
require_paths:
|
86
107
|
- lib
|
87
108
|
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
88
110
|
requirements:
|
89
111
|
- - ">="
|
90
112
|
- !ruby/object:Gem::Version
|
@@ -94,6 +116,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
116
|
- 1
|
95
117
|
version: 1.8.1
|
96
118
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
97
120
|
requirements:
|
98
121
|
- - ">="
|
99
122
|
- !ruby/object:Gem::Version
|
@@ -103,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
126
|
requirements: []
|
104
127
|
|
105
128
|
rubyforge_project: sxp
|
106
|
-
rubygems_version: 1.3.
|
129
|
+
rubygems_version: 1.3.7
|
107
130
|
signing_key:
|
108
131
|
specification_version: 3
|
109
132
|
summary: A pure-Ruby implementation of a universal S-expression parser.
|