rdf-n3 2.2.0 → 3.1.2
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.
- checksums.yaml +5 -5
- data/README.md +192 -69
- data/UNLICENSE +1 -1
- data/VERSION +1 -1
- data/lib/rdf/n3.rb +11 -8
- data/lib/rdf/n3/algebra.rb +204 -0
- data/lib/rdf/n3/algebra/builtin.rb +79 -0
- data/lib/rdf/n3/algebra/formula.rb +446 -0
- data/lib/rdf/n3/algebra/list/append.rb +42 -0
- data/lib/rdf/n3/algebra/list/first.rb +24 -0
- data/lib/rdf/n3/algebra/list/in.rb +48 -0
- data/lib/rdf/n3/algebra/list/last.rb +24 -0
- data/lib/rdf/n3/algebra/list/length.rb +24 -0
- data/lib/rdf/n3/algebra/list/member.rb +44 -0
- data/lib/rdf/n3/algebra/list_operator.rb +83 -0
- data/lib/rdf/n3/algebra/log/conclusion.rb +65 -0
- data/lib/rdf/n3/algebra/log/conjunction.rb +36 -0
- data/lib/rdf/n3/algebra/log/content.rb +34 -0
- data/lib/rdf/n3/algebra/log/equal_to.rb +34 -0
- data/lib/rdf/n3/algebra/log/implies.rb +102 -0
- data/lib/rdf/n3/algebra/log/includes.rb +70 -0
- data/lib/rdf/n3/algebra/log/n3_string.rb +34 -0
- data/lib/rdf/n3/algebra/log/not_equal_to.rb +23 -0
- data/lib/rdf/n3/algebra/log/not_includes.rb +27 -0
- data/lib/rdf/n3/algebra/log/output_string.rb +40 -0
- data/lib/rdf/n3/algebra/log/parsed_as_n3.rb +36 -0
- data/lib/rdf/n3/algebra/log/semantics.rb +40 -0
- data/lib/rdf/n3/algebra/math/absolute_value.rb +36 -0
- data/lib/rdf/n3/algebra/math/acos.rb +26 -0
- data/lib/rdf/n3/algebra/math/acosh.rb +26 -0
- data/lib/rdf/n3/algebra/math/asin.rb +26 -0
- data/lib/rdf/n3/algebra/math/asinh.rb +26 -0
- data/lib/rdf/n3/algebra/math/atan.rb +26 -0
- data/lib/rdf/n3/algebra/math/atanh.rb +26 -0
- data/lib/rdf/n3/algebra/math/ceiling.rb +28 -0
- data/lib/rdf/n3/algebra/math/cos.rb +40 -0
- data/lib/rdf/n3/algebra/math/cosh.rb +38 -0
- data/lib/rdf/n3/algebra/math/difference.rb +40 -0
- data/lib/rdf/n3/algebra/math/equal_to.rb +54 -0
- data/lib/rdf/n3/algebra/math/exponentiation.rb +35 -0
- data/lib/rdf/n3/algebra/math/floor.rb +28 -0
- data/lib/rdf/n3/algebra/math/greater_than.rb +41 -0
- data/lib/rdf/n3/algebra/math/less_than.rb +41 -0
- data/lib/rdf/n3/algebra/math/negation.rb +38 -0
- data/lib/rdf/n3/algebra/math/not_equal_to.rb +25 -0
- data/lib/rdf/n3/algebra/math/not_greater_than.rb +25 -0
- data/lib/rdf/n3/algebra/math/not_less_than.rb +25 -0
- data/lib/rdf/n3/algebra/math/product.rb +20 -0
- data/lib/rdf/n3/algebra/math/quotient.rb +36 -0
- data/lib/rdf/n3/algebra/math/remainder.rb +35 -0
- data/lib/rdf/n3/algebra/math/rounded.rb +26 -0
- data/lib/rdf/n3/algebra/math/sin.rb +40 -0
- data/lib/rdf/n3/algebra/math/sinh.rb +38 -0
- data/lib/rdf/n3/algebra/math/sum.rb +40 -0
- data/lib/rdf/n3/algebra/math/tan.rb +40 -0
- data/lib/rdf/n3/algebra/math/tanh.rb +38 -0
- data/lib/rdf/n3/algebra/not_implemented.rb +13 -0
- data/lib/rdf/n3/algebra/resource_operator.rb +123 -0
- data/lib/rdf/n3/algebra/str/concatenation.rb +27 -0
- data/lib/rdf/n3/algebra/str/contains.rb +33 -0
- data/lib/rdf/n3/algebra/str/contains_ignoring_case.rb +33 -0
- data/lib/rdf/n3/algebra/str/ends_with.rb +33 -0
- data/lib/rdf/n3/algebra/str/equal_ignoring_case.rb +34 -0
- data/lib/rdf/n3/algebra/str/format.rb +17 -0
- data/lib/rdf/n3/algebra/str/greater_than.rb +38 -0
- data/lib/rdf/n3/algebra/str/less_than.rb +33 -0
- data/lib/rdf/n3/algebra/str/matches.rb +37 -0
- data/lib/rdf/n3/algebra/str/not_equal_ignoring_case.rb +17 -0
- data/lib/rdf/n3/algebra/str/not_greater_than.rb +17 -0
- data/lib/rdf/n3/algebra/str/not_less_than.rb +17 -0
- data/lib/rdf/n3/algebra/str/not_matches.rb +18 -0
- data/lib/rdf/n3/algebra/str/replace.rb +35 -0
- data/lib/rdf/n3/algebra/str/scrape.rb +35 -0
- data/lib/rdf/n3/algebra/str/starts_with.rb +33 -0
- data/lib/rdf/n3/algebra/time/day.rb +35 -0
- data/lib/rdf/n3/algebra/time/day_of_week.rb +27 -0
- data/lib/rdf/n3/algebra/time/gm_time.rb +29 -0
- data/lib/rdf/n3/algebra/time/hour.rb +35 -0
- data/lib/rdf/n3/algebra/time/in_seconds.rb +59 -0
- data/lib/rdf/n3/algebra/time/local_time.rb +29 -0
- data/lib/rdf/n3/algebra/time/minute.rb +35 -0
- data/lib/rdf/n3/algebra/time/month.rb +35 -0
- data/lib/rdf/n3/algebra/time/second.rb +35 -0
- data/lib/rdf/n3/algebra/time/timezone.rb +36 -0
- data/lib/rdf/n3/algebra/time/year.rb +29 -0
- data/lib/rdf/n3/extensions.rb +221 -0
- data/lib/rdf/n3/format.rb +66 -1
- data/lib/rdf/n3/list.rb +630 -0
- data/lib/rdf/n3/reader.rb +834 -492
- data/lib/rdf/n3/reasoner.rb +282 -0
- data/lib/rdf/n3/refinements.rb +178 -0
- data/lib/rdf/n3/repository.rb +332 -0
- data/lib/rdf/n3/terminals.rb +80 -0
- data/lib/rdf/n3/vocab.rb +36 -3
- data/lib/rdf/n3/writer.rb +476 -239
- metadata +187 -68
- data/AUTHORS +0 -1
- data/History.markdown +0 -99
- data/lib/rdf/n3/patches/array_hacks.rb +0 -53
- data/lib/rdf/n3/reader/meta.rb +0 -641
- data/lib/rdf/n3/reader/parser.rb +0 -237
data/lib/rdf/n3/writer.rb
CHANGED
@@ -1,42 +1,39 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
module RDF::N3
|
3
3
|
##
|
4
|
-
# A
|
4
|
+
# A Notation-3 serialiser in Ruby
|
5
5
|
#
|
6
6
|
# Note that the natural interface is to write a whole graph at a time.
|
7
7
|
# Writing statements or Triples will create a graph to add them to
|
8
8
|
# and then serialize the graph.
|
9
9
|
#
|
10
|
-
# @example Obtaining a
|
10
|
+
# @example Obtaining a N3 writer class
|
11
11
|
# RDF::Writer.for(:n3) #=> RDF::N3::Writer
|
12
12
|
# RDF::Writer.for("etc/test.n3")
|
13
|
-
# RDF::Writer.for("etc/test.ttl")
|
14
13
|
# RDF::Writer.for(file_name: "etc/test.n3")
|
15
|
-
# RDF::Writer.for(file_name: "etc/test.ttl")
|
16
14
|
# RDF::Writer.for(file_extension: "n3")
|
17
|
-
# RDF::Writer.for(file_extension: "ttl")
|
18
15
|
# RDF::Writer.for(content_type: "text/n3")
|
19
16
|
#
|
20
|
-
# @example Serializing RDF graph into an
|
17
|
+
# @example Serializing RDF graph into an N3 file
|
21
18
|
# RDF::N3::Writer.open("etc/test.n3") do |writer|
|
22
19
|
# writer << graph
|
23
20
|
# end
|
24
21
|
#
|
25
|
-
# @example Serializing RDF statements into an
|
22
|
+
# @example Serializing RDF statements into an N3 file
|
26
23
|
# RDF::N3::Writer.open("etc/test.n3") do |writer|
|
27
24
|
# graph.each_statement do |statement|
|
28
25
|
# writer << statement
|
29
26
|
# end
|
30
27
|
# end
|
31
28
|
#
|
32
|
-
# @example Serializing RDF statements into an
|
29
|
+
# @example Serializing RDF statements into an N3 string
|
33
30
|
# RDF::N3::Writer.buffer do |writer|
|
34
31
|
# graph.each_statement do |statement|
|
35
32
|
# writer << statement
|
36
33
|
# end
|
37
34
|
# end
|
38
35
|
#
|
39
|
-
# The writer will add prefix definitions, and use them for creating @prefix definitions, and minting
|
36
|
+
# The writer will add prefix definitions, and use them for creating @prefix definitions, and minting pnames
|
40
37
|
#
|
41
38
|
# @example Creating @base and @prefix definitions in output
|
42
39
|
# RDF::N3::Writer.buffer(base_uri: "http://example.com/", prefixes: {
|
@@ -52,12 +49,17 @@ module RDF::N3
|
|
52
49
|
class Writer < RDF::Writer
|
53
50
|
format RDF::N3::Format
|
54
51
|
include RDF::Util::Logger
|
55
|
-
|
52
|
+
include Terminals
|
53
|
+
using Refinements
|
56
54
|
|
57
|
-
# @return [
|
55
|
+
# @return [RDF::Repository] Repository of statements serialized
|
56
|
+
attr_accessor :repo
|
57
|
+
|
58
|
+
# @return [RDF::Graph] Graph being serialized
|
58
59
|
attr_accessor :graph
|
59
|
-
|
60
|
-
|
60
|
+
|
61
|
+
# @return [Array<RDF::Node>] formulae names
|
62
|
+
attr_accessor :formula_names
|
61
63
|
|
62
64
|
##
|
63
65
|
# N3 Writer options
|
@@ -78,7 +80,7 @@ module RDF::N3
|
|
78
80
|
end
|
79
81
|
|
80
82
|
##
|
81
|
-
# Initializes the
|
83
|
+
# Initializes the N3 writer instance.
|
82
84
|
#
|
83
85
|
# @param [IO, File] output
|
84
86
|
# the output stream
|
@@ -105,11 +107,15 @@ module RDF::N3
|
|
105
107
|
# @yieldreturn [void]
|
106
108
|
# @yield [writer]
|
107
109
|
# @yieldparam [RDF::Writer] writer
|
108
|
-
def initialize(output = $stdout, options
|
110
|
+
def initialize(output = $stdout, **options, &block)
|
111
|
+
@repo = RDF::N3::Repository.new
|
112
|
+
@uri_to_pname = {}
|
113
|
+
@uri_to_prefix = {}
|
109
114
|
super do
|
110
|
-
|
111
|
-
|
112
|
-
|
115
|
+
if base_uri
|
116
|
+
@uri_to_prefix[base_uri.to_s.end_with?('#', '/') ? base_uri : RDF::URI("#{base_uri}#")] = nil
|
117
|
+
end
|
118
|
+
reset
|
113
119
|
if block_given?
|
114
120
|
case block.arity
|
115
121
|
when 0 then instance_eval(&block)
|
@@ -128,7 +134,19 @@ module RDF::N3
|
|
128
134
|
# @raise [NotImplementedError] unless implemented in subclass
|
129
135
|
# @abstract
|
130
136
|
def write_triple(subject, predicate, object)
|
131
|
-
|
137
|
+
repo.insert(RDF::Statement(subject, predicate, object))
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Adds a quad to be serialized
|
142
|
+
# @param [RDF::Resource] subject
|
143
|
+
# @param [RDF::URI] predicate
|
144
|
+
# @param [RDF::Value] object
|
145
|
+
# @param [RDF::Resource] graph_name
|
146
|
+
# @return [void]
|
147
|
+
def write_quad(subject, predicate, object, graph_name)
|
148
|
+
statement = RDF::Statement.new(subject, predicate, object, graph_name: graph_name)
|
149
|
+
repo.insert(statement)
|
132
150
|
end
|
133
151
|
|
134
152
|
##
|
@@ -138,28 +156,51 @@ module RDF::N3
|
|
138
156
|
# @see #write_triple
|
139
157
|
def write_epilogue
|
140
158
|
@max_depth = @options[:max_depth] || 3
|
141
|
-
@base_uri = RDF::URI(@options[:base_uri])
|
142
159
|
|
143
160
|
self.reset
|
144
161
|
|
145
|
-
log_debug
|
162
|
+
log_debug("\nserialize: repo:") {repo.size}
|
146
163
|
|
147
164
|
preprocess
|
165
|
+
|
148
166
|
start_document
|
149
167
|
|
150
|
-
|
151
|
-
|
152
|
-
|
168
|
+
@formula_names = repo.graph_names(unique: true)
|
169
|
+
|
170
|
+
with_graph(nil) do
|
171
|
+
count = 0
|
172
|
+
order_subjects.each do |subject|
|
173
|
+
unless is_done?(subject)
|
174
|
+
statement(subject, count)
|
175
|
+
count += 1
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Output any formulae not already serialized using owl:sameAs
|
180
|
+
formula_names.each do |graph_name|
|
181
|
+
next if graph_done?(graph_name)
|
182
|
+
|
183
|
+
# Add graph_name to @formulae
|
184
|
+
@formulae[graph_name] = true
|
185
|
+
|
186
|
+
log_debug {"formula(#{graph_name})"}
|
187
|
+
@output.write("\n#{indent}")
|
188
|
+
p_term(graph_name, :subject)
|
189
|
+
@output.write(" ")
|
190
|
+
predicate(RDF::OWL.sameAs)
|
191
|
+
@output.write(" ")
|
192
|
+
formula(graph_name, :graph_name)
|
193
|
+
@output.write(" .\n")
|
153
194
|
end
|
154
195
|
end
|
155
196
|
|
156
197
|
super
|
157
198
|
end
|
158
|
-
|
159
|
-
# Return a
|
199
|
+
|
200
|
+
# Return a pname for the URI, or nil. Adds namespace of pname to defined prefixes
|
160
201
|
# @param [RDF::Resource] resource
|
161
202
|
# @return [String, nil] value to use to identify URI
|
162
|
-
def
|
203
|
+
def get_pname(resource)
|
163
204
|
case resource
|
164
205
|
when RDF::Node
|
165
206
|
return options[:unique_bnodes] ? resource.to_unique_base : resource.to_base
|
@@ -169,56 +210,55 @@ module RDF::N3
|
|
169
210
|
return nil
|
170
211
|
end
|
171
212
|
|
172
|
-
log_debug {"
|
173
|
-
|
174
|
-
when @
|
175
|
-
return @
|
213
|
+
#log_debug {"get_pname(#{resource}), std?}"}
|
214
|
+
pname = case
|
215
|
+
when @uri_to_pname.key?(uri)
|
216
|
+
return @uri_to_pname[uri]
|
176
217
|
when u = @uri_to_prefix.keys.detect {|u| uri.index(u.to_s) == 0}
|
177
218
|
# Use a defined prefix
|
178
219
|
prefix = @uri_to_prefix[u]
|
179
|
-
|
180
|
-
|
181
|
-
|
220
|
+
unless u.to_s.empty?
|
221
|
+
prefix(prefix, u) unless u.to_s.empty?
|
222
|
+
#log_debug("get_pname") {"add prefix #{prefix.inspect} => #{u}"}
|
223
|
+
uri.sub(u.to_s, "#{prefix}:")
|
224
|
+
end
|
182
225
|
when @options[:standard_prefixes] && vocab = RDF::Vocabulary.each.to_a.detect {|v| uri.index(v.to_uri.to_s) == 0}
|
183
226
|
prefix = vocab.__name__.to_s.split('::').last.downcase
|
184
227
|
@uri_to_prefix[vocab.to_uri.to_s] = prefix
|
185
228
|
prefix(prefix, vocab.to_uri) # Define for output
|
186
|
-
log_debug {"
|
229
|
+
#log_debug {"get_pname: add standard prefix #{prefix.inspect} => #{vocab.to_uri}"}
|
187
230
|
uri.sub(vocab.to_uri.to_s, "#{prefix}:")
|
188
231
|
else
|
189
232
|
nil
|
190
233
|
end
|
191
|
-
|
192
|
-
# Make sure
|
193
|
-
if
|
194
|
-
md =
|
195
|
-
|
234
|
+
|
235
|
+
# Make sure pname is a valid pname
|
236
|
+
if pname
|
237
|
+
md = PNAME_LN.match(pname) || PNAME_NS.match(pname)
|
238
|
+
pname = nil unless md.to_s.length == pname.length
|
196
239
|
end
|
197
240
|
|
198
|
-
@
|
199
|
-
rescue Addressable::URI::InvalidURIError => e
|
200
|
-
raise RDF::WriterError, "Invalid URI #{resource.inspect}: #{e.message}"
|
241
|
+
@uri_to_pname[uri] = pname
|
201
242
|
end
|
202
|
-
|
243
|
+
|
203
244
|
# Take a hash from predicate uris to lists of values.
|
204
245
|
# Sort the lists of values. Return a sorted list of properties.
|
205
|
-
# @param [Hash{
|
206
|
-
# @return [Array<
|
246
|
+
# @param [Hash{RDF::Term => Array<RDF::Term>}] properties A hash of Property to Resource mappings
|
247
|
+
# @return [Array<RDF::Term>}] Ordered list of properties. Uses predicate_order.
|
207
248
|
def sort_properties(properties)
|
208
249
|
# Make sorted list of properties
|
209
250
|
prop_list = []
|
210
|
-
|
251
|
+
|
211
252
|
predicate_order.each do |prop|
|
212
|
-
next unless properties
|
213
|
-
prop_list << prop
|
253
|
+
next unless properties.key?(prop)
|
254
|
+
prop_list << prop
|
214
255
|
end
|
215
|
-
|
256
|
+
|
216
257
|
properties.keys.sort.each do |prop|
|
217
|
-
next if prop_list.include?(prop
|
218
|
-
prop_list << prop
|
258
|
+
next if prop_list.include?(prop)
|
259
|
+
prop_list << prop
|
219
260
|
end
|
220
|
-
|
221
|
-
log_debug {"sort_properties: #{prop_list.join(', ')}"}
|
261
|
+
|
222
262
|
prop_list
|
223
263
|
end
|
224
264
|
|
@@ -228,15 +268,19 @@ module RDF::N3
|
|
228
268
|
# @param [RDF::Literal, String, #to_s] literal
|
229
269
|
# @param [Hash{Symbol => Object}] options
|
230
270
|
# @return [String]
|
231
|
-
def format_literal(literal, options
|
271
|
+
def format_literal(literal, **options)
|
232
272
|
literal = literal.dup.canonicalize! if @options[:canonicalize]
|
233
273
|
case literal
|
234
274
|
when RDF::Literal
|
235
|
-
case literal.datatype
|
275
|
+
case literal.valid? ? literal.datatype : false
|
236
276
|
when RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.decimal
|
237
|
-
literal.to_s
|
277
|
+
literal.canonicalize.to_s
|
238
278
|
when RDF::XSD.double
|
239
|
-
literal.
|
279
|
+
if literal.nan? || literal.infinite?
|
280
|
+
quoted(literal.value) + "^^#{format_uri(literal.datatype)}"
|
281
|
+
else
|
282
|
+
literal.canonicalize.to_s
|
283
|
+
end
|
240
284
|
else
|
241
285
|
text = quoted(literal.value)
|
242
286
|
text << "@#{literal.language}" if literal.has_language?
|
@@ -247,48 +291,59 @@ module RDF::N3
|
|
247
291
|
quoted(literal.to_s)
|
248
292
|
end
|
249
293
|
end
|
250
|
-
|
294
|
+
|
251
295
|
##
|
252
|
-
# Returns the
|
296
|
+
# Returns the N3 representation of a URI reference.
|
253
297
|
#
|
254
298
|
# @param [RDF::URI] uri
|
255
299
|
# @param [Hash{Symbol => Object}] options
|
256
300
|
# @return [String]
|
257
|
-
def format_uri(uri, options
|
258
|
-
md = relativize(
|
259
|
-
log_debug {"
|
260
|
-
md != uri.to_s ? "<#{md}>" : (
|
301
|
+
def format_uri(uri, **options)
|
302
|
+
md = uri == base_uri ? '' : uri.relativize(base_uri)
|
303
|
+
log_debug("relativize") {"#{uri.to_sxp} => <#{md.inspect}>"} if md != uri.to_s
|
304
|
+
md != uri.to_s ? "<#{md}>" : (get_pname(uri) || "<#{uri}>")
|
261
305
|
end
|
262
|
-
|
306
|
+
|
263
307
|
##
|
264
|
-
# Returns the
|
308
|
+
# Returns the N3 representation of a blank node.
|
265
309
|
#
|
266
310
|
# @param [RDF::Node] node
|
267
311
|
# @param [Hash{Symbol => Object}] options
|
268
312
|
# @return [String]
|
269
|
-
def format_node(node, options
|
270
|
-
|
313
|
+
def format_node(node, **options)
|
314
|
+
if node.id.match(/^([^_]+)_[^_]+_([^_]+)$/)
|
315
|
+
sn, seq = $1, $2.to_i
|
316
|
+
seq = nil if seq == 0
|
317
|
+
"_:#{sn}#{seq}"
|
318
|
+
elsif options[:unique_bnodes]
|
319
|
+
node.to_unique_base
|
320
|
+
else
|
321
|
+
node.to_base
|
322
|
+
end
|
271
323
|
end
|
272
|
-
|
324
|
+
|
273
325
|
protected
|
274
326
|
# Output @base and @prefix definitions
|
275
327
|
def start_document
|
276
|
-
@
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
log_debug {"start_document: #{prefixes.inspect}"}
|
328
|
+
@output.write("@base <#{base_uri}> .\n") unless base_uri.to_s.empty?
|
329
|
+
|
330
|
+
log_debug("start_document: prefixes") { prefixes.inspect}
|
281
331
|
prefixes.keys.sort_by(&:to_s).each do |prefix|
|
282
|
-
@output.write("
|
332
|
+
@output.write("@prefix #{prefix}: <#{prefixes[prefix]}> .\n")
|
333
|
+
end
|
334
|
+
|
335
|
+
# Universals and extentials at top-level
|
336
|
+
unless @universals.empty?
|
337
|
+
log_debug("start_document: universals") { @universals.inspect}
|
338
|
+
terms = @universals.map {|v| format_uri(RDF::URI(v.name.to_s))}
|
339
|
+
@output.write("@forAll #{terms.join(', ')} .\n")
|
340
|
+
end
|
341
|
+
|
342
|
+
unless @existentials.empty?
|
343
|
+
log_debug("start_document: existentials") { @existentials.inspect}
|
344
|
+
terms = @existentials.map {|v| format_uri(RDF::URI(v.name.to_s.sub(/_ext$/, '')))}
|
345
|
+
@output.write("@forSome #{terms.join(', ')} .\n")
|
283
346
|
end
|
284
|
-
end
|
285
|
-
|
286
|
-
# If base_uri is defined, use it to try to make uri relative
|
287
|
-
# @param [#to_s] uri
|
288
|
-
# @return [String]
|
289
|
-
def relativize(uri)
|
290
|
-
uri = uri.to_s
|
291
|
-
base_uri ? uri.sub(base_uri.to_s, "") : uri
|
292
347
|
end
|
293
348
|
|
294
349
|
# Defines rdf:type of subjects to be emitted at the beginning of the graph. Defaults to rdfs:Class
|
@@ -298,8 +353,18 @@ module RDF::N3
|
|
298
353
|
# Defines order of predicates to to emit at begninning of a resource description. Defaults to
|
299
354
|
# [rdf:type, rdfs:label, dc:title]
|
300
355
|
# @return [Array<URI>]
|
301
|
-
def predicate_order
|
302
|
-
|
356
|
+
def predicate_order
|
357
|
+
[
|
358
|
+
RDF.type,
|
359
|
+
RDF::RDFS.label,
|
360
|
+
RDF::RDFS.comment,
|
361
|
+
RDF::URI("http://purl.org/dc/terms/title"),
|
362
|
+
RDF::URI("http://purl.org/dc/terms/description"),
|
363
|
+
RDF::OWL.sameAs,
|
364
|
+
RDF::N3::Log.implies
|
365
|
+
]
|
366
|
+
end
|
367
|
+
|
303
368
|
# Order subjects for output. Override this to output subjects in another order.
|
304
369
|
#
|
305
370
|
# Uses #top_classes and #base_uri.
|
@@ -307,36 +372,46 @@ module RDF::N3
|
|
307
372
|
def order_subjects
|
308
373
|
seen = {}
|
309
374
|
subjects = []
|
310
|
-
|
375
|
+
|
311
376
|
# Start with base_uri
|
312
|
-
if base_uri && @subjects.keys.include?(base_uri)
|
377
|
+
if base_uri && @subjects.keys.select(&:uri?).include?(base_uri)
|
313
378
|
subjects << base_uri
|
314
379
|
seen[base_uri] = true
|
315
380
|
end
|
316
|
-
|
317
|
-
|
381
|
+
|
318
382
|
# Add distinguished classes
|
319
383
|
top_classes.each do |class_uri|
|
320
|
-
graph.query(predicate: RDF.type, object: class_uri).
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
384
|
+
graph.query({predicate: RDF.type, object: class_uri}).
|
385
|
+
map {|st| st.subject}.sort.uniq.each do |subject|
|
386
|
+
log_debug("order_subjects") {subject.to_sxp}
|
387
|
+
subjects << subject
|
388
|
+
seen[subject] = true
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# Add formulae which are subjects in this graph
|
393
|
+
@formulae.each_key do |bn|
|
394
|
+
next unless @subjects.key?(bn)
|
395
|
+
subjects << bn
|
396
|
+
seen[bn] = true
|
397
|
+
end
|
398
|
+
|
399
|
+
# Mark as seen lists that are part of another list
|
400
|
+
@lists.values.flatten.each do |v|
|
401
|
+
seen[v] = true if @lists.key?(v)
|
325
402
|
end
|
326
|
-
|
327
|
-
|
403
|
+
|
404
|
+
list_elements = [] # Lists may be top-level elements
|
405
|
+
|
328
406
|
# Sort subjects by resources over bnodes, ref_counts and the subject URI itself
|
329
|
-
recursable = @subjects.keys.
|
407
|
+
recursable = (@subjects.keys - list_elements).
|
330
408
|
select {|s| !seen.include?(s)}.
|
331
409
|
map {|r| [r.node? ? 1 : 0, ref_count(r), r]}.
|
332
410
|
sort
|
333
|
-
|
334
|
-
log_debug {"subjects3: #{subjects.inspect}"}
|
411
|
+
|
335
412
|
subjects += recursable.map{|r| r.last}
|
336
|
-
log_debug {"subjects4: #{subjects.inspect}"}
|
337
|
-
subjects
|
338
413
|
end
|
339
|
-
|
414
|
+
|
340
415
|
# Perform any preprocessing of statements required
|
341
416
|
def preprocess
|
342
417
|
# Load defined prefixes
|
@@ -347,50 +422,52 @@ module RDF::N3
|
|
347
422
|
|
348
423
|
prefix(nil, @options[:default_namespace]) if @options[:default_namespace]
|
349
424
|
|
350
|
-
@
|
425
|
+
@options[:prefixes] = {} # Will define actual used when matched
|
426
|
+
repo.each {|statement| preprocess_statement(statement)}
|
427
|
+
|
428
|
+
vars = repo.enum_term.to_a.uniq.select {|r| r.is_a?(RDF::Query::Variable) && !r.to_s.end_with?('_quick')}
|
429
|
+
@universals = vars.reject(&:existential?)
|
430
|
+
@existentials = vars - @universals
|
351
431
|
end
|
352
|
-
|
432
|
+
|
353
433
|
# Perform any statement preprocessing required. This is used to perform reference counts and determine required
|
354
434
|
# prefixes.
|
355
435
|
# @param [Statement] statement
|
356
436
|
def preprocess_statement(statement)
|
357
|
-
#log_debug
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
@references.fetch(node, 0)
|
437
|
+
#log_debug("preprocess") {statement.inspect}
|
438
|
+
|
439
|
+
# Pre-fetch pnames, to fill prefixes
|
440
|
+
get_pname(statement.subject)
|
441
|
+
get_pname(statement.predicate)
|
442
|
+
get_pname(statement.object)
|
443
|
+
get_pname(statement.object.datatype) if statement.object.literal? && statement.object.datatype
|
444
|
+
end
|
445
|
+
|
446
|
+
# Perform graph-specific preprocessing
|
447
|
+
# @param [Statement] statement
|
448
|
+
def preprocess_graph_statement(statement)
|
449
|
+
bump_reference(statement.object)
|
450
|
+
# Count properties of this subject
|
451
|
+
@subjects[statement.subject] ||= {}
|
452
|
+
@subjects[statement.subject][statement.predicate] ||= 0
|
453
|
+
@subjects[statement.subject][statement.predicate] += 1
|
375
454
|
end
|
376
455
|
|
377
456
|
# Returns indent string multiplied by the depth
|
378
457
|
# @param [Integer] modifier Increase depth by specified amount
|
379
458
|
# @return [String] A number of spaces, depending on current depth
|
380
459
|
def indent(modifier = 0)
|
381
|
-
" " * (@
|
460
|
+
" " * (@options.fetch(:log_depth, log_depth) * 2 + modifier)
|
382
461
|
end
|
383
462
|
|
384
463
|
# Reset internal helper instance variables
|
385
464
|
def reset
|
386
|
-
@
|
465
|
+
@universals, @existentials = [], []
|
387
466
|
@lists = {}
|
388
|
-
@namespaces = {}
|
389
467
|
@references = {}
|
390
468
|
@serialized = {}
|
469
|
+
@graphs = {}
|
391
470
|
@subjects = {}
|
392
|
-
@shortNames = {}
|
393
|
-
@started = false
|
394
471
|
end
|
395
472
|
|
396
473
|
##
|
@@ -400,7 +477,7 @@ module RDF::N3
|
|
400
477
|
# @return [String]
|
401
478
|
def quoted(string)
|
402
479
|
if string.to_s.match(/[\t\n\r]/)
|
403
|
-
string = string.gsub('\\', '
|
480
|
+
string = string.gsub('\\', '\\\\\\\\').gsub('"""', '\\"\\"\\"')
|
404
481
|
%("""#{string}""")
|
405
482
|
else
|
406
483
|
"\"#{escaped(string)}\""
|
@@ -408,153 +485,313 @@ module RDF::N3
|
|
408
485
|
end
|
409
486
|
|
410
487
|
private
|
411
|
-
|
488
|
+
|
412
489
|
# Checks if l is a valid RDF list, i.e. no nodes have other properties.
|
413
|
-
def
|
414
|
-
|
415
|
-
return (l.node? && RDF::List.new(subject: l, graph: @graph).valid?) || l == RDF.nil
|
416
|
-
end
|
417
|
-
|
418
|
-
def do_list(l)
|
419
|
-
list = RDF::List.new(subject: l, graph: @graph)
|
420
|
-
log_debug {"do_list: #{list.inspect}"}
|
421
|
-
position = :subject
|
422
|
-
list.each_statement do |st|
|
423
|
-
next unless st.predicate == RDF.first
|
424
|
-
log_debug {" list this: #{st.subject} first: #{st.object}[#{position}]"}
|
425
|
-
path(st.object, position)
|
426
|
-
subject_done(st.subject)
|
427
|
-
position = :object
|
428
|
-
end
|
429
|
-
end
|
430
|
-
|
431
|
-
def p_list(node, position)
|
432
|
-
return false if !is_valid_list(node)
|
433
|
-
#log_debug {"p_list: #{node.inspect}, #{position}"}
|
434
|
-
|
435
|
-
@output.write(position == :subject ? "(" : " (")
|
436
|
-
@depth += 2
|
437
|
-
do_list(node)
|
438
|
-
@depth -= 2
|
439
|
-
@output.write(')')
|
490
|
+
def collection?(l)
|
491
|
+
return @lists.key?(l) || l.list?
|
440
492
|
end
|
441
|
-
|
442
|
-
def
|
443
|
-
node
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
493
|
+
|
494
|
+
def collection(node, position)
|
495
|
+
return false if !collection?(node)
|
496
|
+
log_debug("collection") do
|
497
|
+
"#{node.to_sxp}, " +
|
498
|
+
"pos: #{position}, " +
|
499
|
+
"rc: #{ref_count(node)}"
|
500
|
+
end
|
501
|
+
|
502
|
+
@output.write("(")
|
503
|
+
log_depth do
|
504
|
+
list = node.list? ? node : @lists[node]
|
505
|
+
log_debug("collection") {list.inspect}
|
506
|
+
subject_done(RDF.nil)
|
507
|
+
subject_done(node)
|
508
|
+
index = 0
|
509
|
+
list.each do |li|
|
510
|
+
log_debug("(list first)") {"#{li}[#{position}]"}
|
511
|
+
@output.write(" ") if index > 0
|
512
|
+
path(li, :object)
|
513
|
+
subject_done(li)
|
514
|
+
index += 1
|
515
|
+
end
|
516
|
+
end
|
517
|
+
@output.write(')')
|
460
518
|
end
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
519
|
+
|
520
|
+
# Default singular resource representation.
|
521
|
+
def p_term(resource, position)
|
522
|
+
#log_debug("p_term") {"#{resource.to_sxp}, #{position}"}
|
523
|
+
l = if resource.is_a?(RDF::Query::Variable)
|
524
|
+
if resource.to_s.end_with?('_quick')
|
525
|
+
'?' + RDF::URI(resource.name).fragment.sub(/_quick$/, '')
|
526
|
+
else
|
527
|
+
format_term(RDF::URI(resource.name.to_s.sub(/_ext$/, '')))
|
528
|
+
end
|
529
|
+
elsif resource == RDF.nil
|
530
|
+
"()"
|
531
|
+
else
|
532
|
+
format_term(resource, **options)
|
533
|
+
end
|
465
534
|
@output.write(l)
|
466
535
|
end
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
536
|
+
|
537
|
+
# Represent a resource in subject, predicate or object position.
|
538
|
+
# Use either collection, blankNodePropertyList or singular resource notation.
|
539
|
+
def path(resource, position)
|
540
|
+
log_debug("path") do
|
541
|
+
"#{resource.to_sxp}, " +
|
471
542
|
"pos: #{position}, " +
|
472
|
-
"
|
473
|
-
"
|
474
|
-
"
|
543
|
+
"{}?: #{formula?(resource, position).inspect}, " +
|
544
|
+
"()?: #{collection?(resource).inspect}, " +
|
545
|
+
"[]?: #{blankNodePropertyList?(resource, position).inspect}, " +
|
546
|
+
"rc: #{ref_count(resource)}"
|
475
547
|
end
|
476
|
-
raise RDF::WriterError, "Cannot serialize
|
548
|
+
raise RDF::WriterError, "Cannot serialize resource '#{resource}'" unless
|
549
|
+
formula(resource, position) ||
|
550
|
+
collection(resource, position) ||
|
551
|
+
blankNodePropertyList(resource, position) ||
|
552
|
+
p_term(resource, position)
|
477
553
|
end
|
478
|
-
|
479
|
-
def
|
480
|
-
log_debug
|
481
|
-
|
482
|
-
|
554
|
+
|
555
|
+
def predicate(resource)
|
556
|
+
log_debug("predicate") {resource.to_sxp}
|
557
|
+
case resource
|
558
|
+
when RDF.type
|
559
|
+
@output.write("a")
|
560
|
+
when RDF::OWL.sameAs
|
561
|
+
@output.write("=")
|
562
|
+
when RDF::N3::Log.implies
|
563
|
+
@output.write("=>")
|
483
564
|
else
|
484
|
-
path(
|
565
|
+
log_depth {path(resource, :predicate)}
|
485
566
|
end
|
486
567
|
end
|
487
|
-
|
488
|
-
|
489
|
-
|
568
|
+
|
569
|
+
# Render an objectList having a common subject and predicate
|
570
|
+
def objectList(objects)
|
571
|
+
log_debug("objectList") {objects.inspect}
|
490
572
|
return if objects.empty?
|
491
573
|
|
492
|
-
|
493
|
-
|
494
|
-
|
574
|
+
log_depth do
|
575
|
+
objects.each_with_index do |obj, i|
|
576
|
+
if i > 0 && (formula?(obj, :object) || blankNodePropertyList?(obj, :object))
|
577
|
+
@output.write ", "
|
578
|
+
elsif i > 0
|
579
|
+
@output.write ",\n#{indent(4)}"
|
580
|
+
end
|
581
|
+
path(obj, :object)
|
582
|
+
end
|
495
583
|
end
|
496
584
|
end
|
497
|
-
|
498
|
-
|
585
|
+
|
586
|
+
# Render a predicateObjectList having a common subject.
|
587
|
+
# @return [Integer] the number of properties serialized
|
588
|
+
def predicateObjectList(subject, from_bpl = false)
|
499
589
|
properties = {}
|
500
|
-
@graph.
|
501
|
-
properties[st.predicate
|
502
|
-
properties[st.predicate.to_s] << st.object
|
590
|
+
@graph.enum_statement.select {|s| s.subject.sameTerm?(subject)}.each do |st|
|
591
|
+
(properties[st.predicate] ||= []) << st.object
|
503
592
|
end
|
504
593
|
|
505
|
-
prop_list = sort_properties(properties)
|
506
|
-
|
507
|
-
|
594
|
+
prop_list = sort_properties(properties)
|
595
|
+
prop_list -= [RDF.first, RDF.rest] if @lists.key?(subject)
|
596
|
+
log_debug("predicateObjectList") { "subject: #{subject.to_sxp}, properties: #{prop_list.join(', ')}" }
|
597
|
+
return 0 if prop_list.empty?
|
508
598
|
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
599
|
+
@output.write("\n#{indent(2)}") if properties.keys.length > 1 && from_bpl
|
600
|
+
log_depth do
|
601
|
+
prop_list.each_with_index do |prop, i|
|
602
|
+
begin
|
603
|
+
@output.write(";\n#{indent(2)}") if i > 0
|
604
|
+
predicate(prop)
|
605
|
+
@output.write(" ")
|
606
|
+
objectList(properties[prop])
|
607
|
+
end
|
516
608
|
end
|
517
609
|
end
|
610
|
+
properties.keys.length
|
611
|
+
end
|
612
|
+
|
613
|
+
# Can subject be represented as a blankNodePropertyList?
|
614
|
+
def blankNodePropertyList?(resource, position)
|
615
|
+
resource.node? &&
|
616
|
+
!formula?(resource, position) &&
|
617
|
+
!collection?(resource) &&
|
618
|
+
(!is_done?(resource) || position == :subject) &&
|
619
|
+
ref_count(resource) == (position == :object ? 1 : 0) &&
|
620
|
+
!repo.has_graph?(resource)
|
621
|
+
end
|
622
|
+
|
623
|
+
def blankNodePropertyList(resource, position)
|
624
|
+
return false unless blankNodePropertyList?(resource, position)
|
625
|
+
|
626
|
+
log_debug("blankNodePropertyList") {resource.to_sxp}
|
627
|
+
subject_done(resource)
|
628
|
+
@output.write((position == :subject ? "\n#{indent}[" : '['))
|
629
|
+
num_props = log_depth {predicateObjectList(resource, true)}
|
630
|
+
@output.write((num_props > 1 ? "\n#{indent(2)}" : "") + (position == :subject ? '] .' : ']'))
|
631
|
+
true
|
518
632
|
end
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
633
|
+
|
634
|
+
# Can subject be represented as a formula?
|
635
|
+
def formula?(resource, position)
|
636
|
+
!!@formulae[resource]
|
637
|
+
end
|
638
|
+
|
639
|
+
def formula(resource, position)
|
640
|
+
return false unless formula?(resource, position)
|
641
|
+
|
642
|
+
log_debug("formula") {resource.to_sxp}
|
643
|
+
subject_done(resource)
|
644
|
+
@output.write('{')
|
645
|
+
count = 0
|
646
|
+
log_depth do
|
647
|
+
with_graph(resource) do
|
648
|
+
order_subjects.each do |subject|
|
649
|
+
unless is_done?(subject)
|
650
|
+
statement(subject, count)
|
651
|
+
count += 1
|
652
|
+
end
|
653
|
+
end
|
654
|
+
end
|
655
|
+
end
|
656
|
+
@output.write((count > 0 ? "#{indent}" : "") + '}')
|
533
657
|
true
|
534
658
|
end
|
535
|
-
|
536
|
-
|
659
|
+
|
660
|
+
# Render triples having the same subject using an explicit subject
|
661
|
+
def triples(subject)
|
537
662
|
@output.write("\n#{indent}")
|
538
663
|
path(subject, :subject)
|
539
|
-
|
540
|
-
|
664
|
+
@output.write(" ")
|
665
|
+
num_props = predicateObjectList(subject)
|
666
|
+
@output.puts("#{num_props > 0 ? ' ' : ''}.")
|
541
667
|
true
|
542
668
|
end
|
543
|
-
|
544
|
-
def statement(subject)
|
545
|
-
log_debug
|
669
|
+
|
670
|
+
def statement(subject, count)
|
671
|
+
log_debug("statement") do
|
672
|
+
"#{subject.to_sxp}, " +
|
673
|
+
"{}?: #{formula?(subject, :subject).inspect}, " +
|
674
|
+
"()?: #{collection?(subject).inspect}, " +
|
675
|
+
"[]?: #{blankNodePropertyList?(subject, :subject).inspect}, "
|
676
|
+
end
|
546
677
|
subject_done(subject)
|
547
|
-
|
548
|
-
@output.
|
678
|
+
blankNodePropertyList(subject, :subject) || triples(subject)
|
679
|
+
@output.puts if count > 0 || graph.graph_name
|
680
|
+
end
|
681
|
+
|
682
|
+
# Return the number of times this node has been referenced in the object position
|
683
|
+
# @return [Integer]
|
684
|
+
def ref_count(node)
|
685
|
+
@references.fetch(node, 0)
|
549
686
|
end
|
550
|
-
|
687
|
+
|
688
|
+
# Increase the reference count of this resource
|
689
|
+
# @param [RDF::Resource] resource
|
690
|
+
# @return [Integer] resulting reference count
|
691
|
+
def bump_reference(resource)
|
692
|
+
@references[resource] = ref_count(resource) + 1
|
693
|
+
end
|
694
|
+
|
551
695
|
def is_done?(subject)
|
552
696
|
@serialized.include?(subject)
|
553
697
|
end
|
554
|
-
|
698
|
+
|
555
699
|
# Mark a subject as done.
|
556
700
|
def subject_done(subject)
|
557
701
|
@serialized[subject] = true
|
558
702
|
end
|
703
|
+
|
704
|
+
def graph_done?(subject)
|
705
|
+
@graphs.include?(subject)
|
706
|
+
end
|
707
|
+
|
708
|
+
# Mark a graph as done.
|
709
|
+
def graph_done(graph_name)
|
710
|
+
@graphs[graph_name] = true
|
711
|
+
end
|
712
|
+
|
713
|
+
# Process a graph projection
|
714
|
+
def with_graph(graph_name)
|
715
|
+
old_lists, @lists = @lists, {}
|
716
|
+
old_references, @references = @references, {}
|
717
|
+
old_serialized, @serialized = @serialized, {}
|
718
|
+
old_subjects, @subjects = @subjects, {}
|
719
|
+
old_graph, @graph = @graph, repo.project_graph(graph_name)
|
720
|
+
old_formulae, @formulae = @formulae, {}
|
721
|
+
|
722
|
+
graph_done(graph_name)
|
723
|
+
|
724
|
+
lists = {}
|
725
|
+
graph.each do |statement|
|
726
|
+
preprocess_graph_statement(statement)
|
727
|
+
[statement.subject, statement.object].each do |resource|
|
728
|
+
@formulae[resource] = true if
|
729
|
+
resource.node? &&
|
730
|
+
(formula_names.include?(resource) || resource.id.start_with?('_form_'))
|
731
|
+
|
732
|
+
# First-class list may have members which are formulae, and need reference counts
|
733
|
+
if resource.list?
|
734
|
+
resource.each_descendant do |term|
|
735
|
+
bump_reference(term)
|
736
|
+
@formulae[term] = true if
|
737
|
+
term.node? &&
|
738
|
+
(formula_names.include?(term) || term.id.start_with?('_form_'))
|
739
|
+
end
|
740
|
+
end
|
741
|
+
end
|
742
|
+
|
743
|
+
# Collect list elements
|
744
|
+
if [RDF.first, RDF.rest].include?(statement.predicate) && statement.subject.node?
|
745
|
+
lists[statement.subject] ||= {}
|
746
|
+
lists[statement.subject][statement.predicate] = statement.object
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
# Remove list entries after head with more than two properties (other than rdf:type)
|
751
|
+
rests = lists.values.map {|props| props[RDF.rest]}
|
752
|
+
|
753
|
+
# Remove non-head lists that have too many properties
|
754
|
+
rests.select do |bn|
|
755
|
+
pc = 0
|
756
|
+
@subjects.fetch(bn, {}).each do |pred, count|
|
757
|
+
next if pred == RDF.type
|
758
|
+
pc += count
|
759
|
+
end
|
760
|
+
lists.delete(bn) if pc > 2
|
761
|
+
end
|
762
|
+
|
763
|
+
# Values for this list element, recursive
|
764
|
+
def list_values(bn, lists)
|
765
|
+
raise "no list" unless lists.has_key?(bn)
|
766
|
+
first, rest = lists[bn][RDF.first], lists[bn][RDF.rest]
|
767
|
+
(rest == RDF.nil ? [] : list_values(rest, lists)).unshift(first)
|
768
|
+
rescue
|
769
|
+
lists.delete(bn)
|
770
|
+
raise $!
|
771
|
+
end
|
772
|
+
|
773
|
+
# Create value arrays for each entry
|
774
|
+
lists.each do |bn, props|
|
775
|
+
begin
|
776
|
+
@lists[bn] = list_values(bn, lists)
|
777
|
+
rescue
|
778
|
+
# Skip this list element, if it raises an exception
|
779
|
+
lists.delete(bn)
|
780
|
+
end
|
781
|
+
end
|
782
|
+
|
783
|
+
# Mark all remaining rests done
|
784
|
+
rests.each {|bn| subject_done(bn) if lists.include?(bn)}
|
785
|
+
|
786
|
+
# Remove entries that are referenced as rdf:rest of some entry
|
787
|
+
lists.each do |bn, props|
|
788
|
+
@lists.delete(props[RDF.rest])
|
789
|
+
end
|
790
|
+
|
791
|
+
# Record nodes in subject or object
|
792
|
+
yield
|
793
|
+
ensure
|
794
|
+
@graph, @lists, @references, @serialized, @subjects, @formulae = old_graph, old_lists, old_references, old_serialized, old_subjects, old_formulae
|
795
|
+
end
|
559
796
|
end
|
560
797
|
end
|