rdf 1.1.17.1 → 1.99.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README +9 -9
- data/VERSION +1 -1
- data/lib/rdf/cli.rb +10 -10
- data/lib/rdf/format.rb +9 -9
- data/lib/rdf/mixin/enumerable.rb +66 -38
- data/lib/rdf/mixin/mutable.rb +11 -5
- data/lib/rdf/mixin/queryable.rb +2 -2
- data/lib/rdf/model/graph.rb +60 -40
- data/lib/rdf/model/list.rb +9 -9
- data/lib/rdf/model/literal/boolean.rb +1 -1
- data/lib/rdf/model/literal/date.rb +1 -1
- data/lib/rdf/model/literal/datetime.rb +5 -5
- data/lib/rdf/model/literal/decimal.rb +1 -1
- data/lib/rdf/model/literal/double.rb +3 -1
- data/lib/rdf/model/literal/integer.rb +1 -1
- data/lib/rdf/model/literal/time.rb +5 -5
- data/lib/rdf/model/literal/token.rb +1 -1
- data/lib/rdf/model/literal.rb +40 -6
- data/lib/rdf/model/node.rb +1 -1
- data/lib/rdf/model/statement.rb +88 -30
- data/lib/rdf/model/term.rb +10 -1
- data/lib/rdf/model/uri.rb +24 -25
- data/lib/rdf/nquads.rb +13 -22
- data/lib/rdf/ntriples/format.rb +4 -5
- data/lib/rdf/ntriples/reader.rb +10 -10
- data/lib/rdf/ntriples/writer.rb +6 -6
- data/lib/rdf/query/pattern.rb +49 -35
- data/lib/rdf/query/solution.rb +1 -1
- data/lib/rdf/query/solutions.rb +4 -4
- data/lib/rdf/query.rb +81 -40
- data/lib/rdf/reader.rb +22 -5
- data/lib/rdf/repository.rb +86 -37
- data/lib/rdf/transaction.rb +41 -20
- data/lib/rdf/util/file.rb +35 -18
- data/lib/rdf/vocab/schema.rb +5129 -5127
- data/lib/rdf/vocabulary.rb +43 -60
- data/lib/rdf/writer.rb +22 -12
- data/lib/rdf.rb +19 -4
- metadata +19 -5
data/lib/rdf/repository.rb
CHANGED
@@ -96,13 +96,19 @@ module RDF
|
|
96
96
|
# @param [Hash{Symbol => Object}] options
|
97
97
|
# @option options [URI, #to_s] :uri (nil)
|
98
98
|
# @option options [String, #to_s] :title (nil)
|
99
|
-
# @option options [Boolean] :
|
99
|
+
# @option options [Boolean] :with_graph_name (true)
|
100
100
|
# Indicates that the repository supports named graphs, otherwise,
|
101
101
|
# only the default graph is supported.
|
102
|
+
# @option options [Boolean] :with_context (true)
|
103
|
+
# Alias for :with_graph_name. :with_context is deprecated in RDF.rb 2.0.
|
102
104
|
# @yield [repository]
|
103
105
|
# @yieldparam [Repository] repository
|
104
106
|
def initialize(options = {}, &block)
|
105
|
-
|
107
|
+
if options[:with_context]
|
108
|
+
warn "[DEPRECATION] the :contexts option to Repository#initialize is deprecated in RDF.rb 2.0, use :graph_name instead. Called from #{Gem.location_of_caller.join(':')}"
|
109
|
+
options[:graph_name] ||= options.delete(:with_context)
|
110
|
+
end
|
111
|
+
@options = {with_graph_name: true}.merge(options)
|
106
112
|
@uri = @options.delete(:uri)
|
107
113
|
@title = @options.delete(:title)
|
108
114
|
|
@@ -140,20 +146,20 @@ module RDF
|
|
140
146
|
#
|
141
147
|
# @example
|
142
148
|
# repository.transaction do |tx|
|
143
|
-
# tx.insert [RDF::URI("http://rubygems.org/gems/rdf"), RDF::
|
149
|
+
# tx.insert [RDF::URI("http://rubygems.org/gems/rdf"), RDF::RDFS.label, "RDF.rb"]
|
144
150
|
# end
|
145
151
|
#
|
146
|
-
# @param [RDF::Resource]
|
152
|
+
# @param [RDF::Resource] graph_name
|
147
153
|
# Context on which to run the transaction, use `false` for the default
|
148
|
-
#
|
154
|
+
# graph_name and `nil` the entire Repository
|
149
155
|
# @yield [tx]
|
150
156
|
# @yieldparam [RDF::Transaction] tx
|
151
157
|
# @yieldreturn [void] ignored
|
152
|
-
# @return [
|
158
|
+
# @return [self]
|
153
159
|
# @see RDF::Transaction
|
154
160
|
# @since 0.3.0
|
155
|
-
def transaction(
|
156
|
-
tx = begin_transaction(
|
161
|
+
def transaction(graph_name = nil, &block)
|
162
|
+
tx = begin_transaction(graph_name)
|
157
163
|
begin
|
158
164
|
case block.arity
|
159
165
|
when 1 then block.call(tx)
|
@@ -177,11 +183,11 @@ module RDF
|
|
177
183
|
# to override this method in order to begin a transaction against the
|
178
184
|
# underlying storage.
|
179
185
|
#
|
180
|
-
# @param [RDF::Resource]
|
186
|
+
# @param [RDF::Resource] graph_name
|
181
187
|
# @return [RDF::Transaction]
|
182
188
|
# @since 0.3.0
|
183
|
-
def begin_transaction(
|
184
|
-
RDF::Transaction.new(:
|
189
|
+
def begin_transaction(graph_name)
|
190
|
+
RDF::Transaction.new(graph: graph_name)
|
185
191
|
end
|
186
192
|
|
187
193
|
##
|
@@ -215,7 +221,7 @@ module RDF
|
|
215
221
|
##
|
216
222
|
# @see RDF::Repository
|
217
223
|
module Implementation
|
218
|
-
|
224
|
+
DEFAULT_GRAPH = false
|
219
225
|
|
220
226
|
##
|
221
227
|
# @private
|
@@ -229,8 +235,11 @@ module RDF
|
|
229
235
|
# @see RDF::Enumerable#supports?
|
230
236
|
def supports?(feature)
|
231
237
|
case feature.to_sym
|
232
|
-
#
|
233
|
-
when :context
|
238
|
+
#statement named graphs
|
239
|
+
when :context
|
240
|
+
warn "[DEPRECATION] the :context feature is deprecated in RDF.rb 2.0; use :graph_name instead. Called from #{Gem.location_of_caller.join(':')}"
|
241
|
+
@options[:with_context] || @options[:with_graph_name]
|
242
|
+
when :graph_name then @options[:with_graph_name]
|
234
243
|
when :inference then false # forward-chaining inference
|
235
244
|
when :validity then @options.fetch(:with_validity, true)
|
236
245
|
else false
|
@@ -249,7 +258,7 @@ module RDF
|
|
249
258
|
# @see RDF::Countable#count
|
250
259
|
def count
|
251
260
|
count = 0
|
252
|
-
@data.each do |
|
261
|
+
@data.each do |g, ss|
|
253
262
|
ss.each do |s, ps|
|
254
263
|
ps.each do |p, os|
|
255
264
|
count += os.size
|
@@ -263,12 +272,12 @@ module RDF
|
|
263
272
|
# @private
|
264
273
|
# @see RDF::Enumerable#has_statement?
|
265
274
|
def has_statement?(statement)
|
266
|
-
s, p, o,
|
267
|
-
|
268
|
-
@data.has_key?(
|
269
|
-
@data[
|
270
|
-
@data[
|
271
|
-
@data[
|
275
|
+
s, p, o, g = statement.to_quad
|
276
|
+
g ||= DEFAULT_GRAPH
|
277
|
+
@data.has_key?(g) &&
|
278
|
+
@data[g].has_key?(s) &&
|
279
|
+
@data[g][s].has_key?(p) &&
|
280
|
+
@data[g][s][p].include?(o)
|
272
281
|
end
|
273
282
|
|
274
283
|
##
|
@@ -280,12 +289,12 @@ module RDF
|
|
280
289
|
# possible concurrent mutations to `@data`, we use `#dup` to make
|
281
290
|
# shallow copies of the nested hashes before beginning the
|
282
291
|
# iteration over their keys and values.
|
283
|
-
@data.dup.each do |
|
292
|
+
@data.dup.each do |g, ss|
|
284
293
|
ss.dup.each do |s, ps|
|
285
294
|
ps.dup.each do |p, os|
|
286
295
|
os.dup.each do |o|
|
287
296
|
# FIXME: yield has better performance, but broken in MRI 2.2: See https://bugs.ruby-lang.org/issues/11451.
|
288
|
-
block.call(RDF::Statement.new(s, p, o, :
|
297
|
+
block.call(RDF::Statement.new(s, p, o, graph_name: g.equal?(DEFAULT_GRAPH) ? nil : g))
|
289
298
|
end
|
290
299
|
end
|
291
300
|
end
|
@@ -298,48 +307,87 @@ module RDF
|
|
298
307
|
##
|
299
308
|
# @private
|
300
309
|
# @see RDF::Enumerable#has_context?
|
310
|
+
# @deprecated Use {#has_graph?} instead.
|
301
311
|
def has_context?(value)
|
312
|
+
warn "[DEPRECATION] Repository#has_context? is deprecated in RDF.rb 2.0, use Repository#has_graph? instead. Called from #{Gem.location_of_caller.join(':')}"
|
313
|
+
has_graph?(value)
|
314
|
+
end
|
315
|
+
|
316
|
+
##
|
317
|
+
# @private
|
318
|
+
# @see RDF::Enumerable#has_graph?
|
319
|
+
def has_graph?(value)
|
302
320
|
@data.keys.include?(value)
|
303
321
|
end
|
322
|
+
##
|
323
|
+
# @private
|
324
|
+
# @see RDF::Enumerable#each_graph
|
325
|
+
def graph_names(options = nil, &block)
|
326
|
+
@data.keys.reject {|g| g == DEFAULT_GRAPH}
|
327
|
+
end
|
304
328
|
|
305
329
|
##
|
306
330
|
# @private
|
307
331
|
# @see RDF::Enumerable#each_context
|
332
|
+
# @deprecated Use {#each_graph} instead.
|
308
333
|
def each_context(&block)
|
334
|
+
warn "[DEPRECATION] Repository#each_context is deprecated in RDF.rb 2.0, use Repository#each_graph instead. Called from #{Gem.location_of_caller.join(':')}"
|
309
335
|
if block_given?
|
310
336
|
contexts = @data.keys
|
311
|
-
contexts.delete(
|
337
|
+
contexts.delete(DEFAULT_GRAPH)
|
312
338
|
contexts.each(&block)
|
313
339
|
end
|
314
340
|
enum_context
|
315
341
|
end
|
316
342
|
|
343
|
+
##
|
344
|
+
# @private
|
345
|
+
# @see RDF::Enumerable#each_graph
|
346
|
+
def each_graph(&block)
|
347
|
+
if block_given?
|
348
|
+
@data.each_key do |gn|
|
349
|
+
yield RDF::Graph.new(gn == DEFAULT_GRAPH ? nil : gn, data: self)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
enum_graph
|
353
|
+
end
|
354
|
+
|
317
355
|
protected
|
318
356
|
|
319
357
|
##
|
320
358
|
# Match elements with eql?, not ==
|
321
|
-
# Context of `false` matches default
|
359
|
+
# Context of `false` matches default graph. Unbound variable matches non-false graph name
|
322
360
|
# @private
|
323
361
|
# @see RDF::Queryable#query
|
324
362
|
def query_pattern(pattern, &block)
|
325
|
-
|
363
|
+
graph_name = pattern.graph_name
|
326
364
|
subject = pattern.subject
|
327
365
|
predicate = pattern.predicate
|
328
366
|
object = pattern.object
|
329
367
|
|
330
|
-
cs = @data.has_key?(
|
368
|
+
cs = @data.has_key?(graph_name) ? {graph_name => @data[graph_name]} : @data.dup
|
331
369
|
cs.each do |c, ss|
|
332
|
-
next unless
|
333
|
-
ss = ss.has_key?(subject)
|
370
|
+
next unless graph_name.nil? || graph_name == false && !c || graph_name.eql?(c)
|
371
|
+
ss = if ss.has_key?(subject)
|
372
|
+
{ subject => ss[subject] }
|
373
|
+
elsif subject.nil? || subject.is_a?(RDF::Query::Variable)
|
374
|
+
ss.dup
|
375
|
+
else
|
376
|
+
[]
|
377
|
+
end
|
334
378
|
ss.each do |s, ps|
|
335
|
-
|
336
|
-
|
379
|
+
ps = if ps.has_key?(predicate)
|
380
|
+
{ predicate => ps[predicate] }
|
381
|
+
elsif predicate.nil? || predicate.is_a?(RDF::Query::Variable)
|
382
|
+
ps.dup
|
383
|
+
else
|
384
|
+
[]
|
385
|
+
end
|
337
386
|
ps.each do |p, os|
|
338
|
-
next unless predicate.nil? || predicate.eql?(p)
|
339
387
|
os = os.dup # TODO: is this really needed?
|
340
388
|
os.each do |o|
|
341
389
|
next unless object.nil? || object.eql?(o)
|
342
|
-
block.call(RDF::Statement.new(s, p, o, :
|
390
|
+
block.call(RDF::Statement.new(s, p, o, graph_name: c.equal?(DEFAULT_GRAPH) ? nil : c))
|
343
391
|
end
|
344
392
|
end
|
345
393
|
end
|
@@ -350,10 +398,11 @@ module RDF
|
|
350
398
|
# @private
|
351
399
|
# @see RDF::Mutable#insert
|
352
400
|
def insert_statement(statement)
|
401
|
+
raise ArgumentError, "Statement #{statement.inspect} is incomplete" if statement.incomplete?
|
353
402
|
unless has_statement?(statement)
|
354
403
|
s, p, o, c = statement.to_quad
|
355
|
-
c =
|
356
|
-
c ||=
|
404
|
+
c = DEFAULT_GRAPH unless supports?(:graph_name)
|
405
|
+
c ||= DEFAULT_GRAPH
|
357
406
|
@data[c] ||= {}
|
358
407
|
@data[c][s] ||= {}
|
359
408
|
@data[c][s][p] ||= []
|
@@ -367,8 +416,8 @@ module RDF
|
|
367
416
|
def delete_statement(statement)
|
368
417
|
if has_statement?(statement)
|
369
418
|
s, p, o, c = statement.to_quad
|
370
|
-
c =
|
371
|
-
c ||=
|
419
|
+
c = DEFAULT_GRAPH unless supports?(:graph_name)
|
420
|
+
c ||= DEFAULT_GRAPH
|
372
421
|
@data[c][s][p].delete(o)
|
373
422
|
@data[c][s].delete(p) if @data[c][s][p].empty?
|
374
423
|
@data[c].delete(s) if @data[c][s].empty?
|
data/lib/rdf/transaction.rb
CHANGED
@@ -13,8 +13,8 @@ module RDF
|
|
13
13
|
# repository = ...
|
14
14
|
# RDF::Transaction.execute(repository) do |tx|
|
15
15
|
# subject = RDF::URI("http://example.org/article")
|
16
|
-
# tx.delete [subject, RDF::
|
17
|
-
# tx.insert [subject, RDF::
|
16
|
+
# tx.delete [subject, RDF::RDFS.label, "Old title"]
|
17
|
+
# tx.insert [subject, RDF::RDFS.label, "New title"]
|
18
18
|
# end
|
19
19
|
#
|
20
20
|
# @since 0.3.0
|
@@ -35,13 +35,25 @@ module RDF
|
|
35
35
|
|
36
36
|
##
|
37
37
|
# Name of this graph, if it is part of an {RDF::Repository}
|
38
|
-
# @!attribute [rw]
|
38
|
+
# @!attribute [rw] graph_name
|
39
39
|
# @return [RDF::Resource]
|
40
40
|
# @since 1.1.0
|
41
|
-
attr_accessor :
|
41
|
+
attr_accessor :graph_name
|
42
42
|
|
43
|
-
|
44
|
-
|
43
|
+
# @deprecated Use {#graph_name} instead.
|
44
|
+
def context
|
45
|
+
warn "[DEPRECATION] Statement#context is being replaced with Statement@graph_name in RDF.rb 2.0. Called from #{Gem.location_of_caller.join(':')}"
|
46
|
+
graph_name
|
47
|
+
end
|
48
|
+
|
49
|
+
# @deprecated Use {#graph_name=} instead.
|
50
|
+
def context=(value)
|
51
|
+
warn "[DEPRECATION] Statement#context= is being replaced with Statement@graph_name= in RDF.rb 2.0. Called from #{Gem.location_of_caller.join(':')}"
|
52
|
+
self.graph_name = value
|
53
|
+
end
|
54
|
+
|
55
|
+
alias_method :graph, :graph_name
|
56
|
+
alias_method :graph=, :graph_name=
|
45
57
|
|
46
58
|
##
|
47
59
|
# RDF statements to delete when executed.
|
@@ -66,19 +78,25 @@ module RDF
|
|
66
78
|
#
|
67
79
|
# @param [Hash{Symbol => Object}] options
|
68
80
|
# @option options [RDF::Resource] :context (nil)
|
81
|
+
# Alias for `:graph_name`. Deprected in RDF.rb 2.0.
|
82
|
+
# @option options [RDF::Resource] :graph_name (nil)
|
69
83
|
# Name of named graph to be affected if `inserts` or `deletes`
|
70
|
-
# do not have a `
|
84
|
+
# do not have a `graph_name`.
|
71
85
|
# @option options [RDF::Resource] :graph (nil)
|
72
|
-
# Alias for `:
|
86
|
+
# Alias for `:graph_name`.
|
73
87
|
# @option options [RDF::Enumerable] :insert (RDF::Graph.new)
|
74
88
|
# @option options [RDF::Enumerable] :delete (RDF::Graph.new)
|
75
89
|
# @yield [tx]
|
76
90
|
# @yieldparam [RDF::Transaction] tx
|
77
91
|
def initialize(options = {}, &block)
|
78
92
|
@options = options.dup
|
79
|
-
|
80
|
-
|
81
|
-
|
93
|
+
if @options.has_key?(:context)
|
94
|
+
warn "[DEPRECATION] the :contexts option to Mutable#load is deprecated in RDF.rb 2.0, use :graph_name instead. Called from #{Gem.location_of_caller.join(':')}"
|
95
|
+
@options[:graph_name] ||= @options.delete(:context)
|
96
|
+
end
|
97
|
+
@graph_name = @options.delete(:graph) || @options.delete(:graph_name)
|
98
|
+
@inserts = @options.delete(:insert) || []
|
99
|
+
@deletes = @options.delete(:delete) || []
|
82
100
|
@inserts.extend(RDF::Enumerable) unless @inserts.kind_of?(RDF::Enumerable)
|
83
101
|
@deletes.extend(RDF::Enumerable) unless @deletes.kind_of?(RDF::Enumerable)
|
84
102
|
|
@@ -112,18 +130,21 @@ module RDF
|
|
112
130
|
def execute(repository, options = {})
|
113
131
|
before_execute(repository, options) if respond_to?(:before_execute)
|
114
132
|
|
115
|
-
deletes.
|
116
|
-
statement =
|
117
|
-
statement.
|
118
|
-
|
133
|
+
dels = deletes.map do |s|
|
134
|
+
statement = s.dup
|
135
|
+
statement.graph_name ||= graph_name
|
136
|
+
statement
|
119
137
|
end
|
120
138
|
|
121
|
-
inserts.
|
122
|
-
statement =
|
123
|
-
statement.
|
124
|
-
|
139
|
+
ins = inserts.map do |s|
|
140
|
+
statement = s.dup
|
141
|
+
statement.graph_name ||= graph_name
|
142
|
+
statement
|
125
143
|
end
|
126
|
-
|
144
|
+
|
145
|
+
repository.delete(*dels) unless dels.empty?
|
146
|
+
repository.insert(*ins) unless ins.empty?
|
147
|
+
|
127
148
|
after_execute(repository, options) if respond_to?(:after_execute)
|
128
149
|
self
|
129
150
|
end
|
data/lib/rdf/util/file.rb
CHANGED
@@ -100,7 +100,6 @@ module RDF; module Util
|
|
100
100
|
# If a Location is returned, it defines the base resource for this file, not it's actual ending location
|
101
101
|
document_options = {
|
102
102
|
base_uri: RDF::URI(response.headers.fetch(:location, base_uri)),
|
103
|
-
charset: Encoding::UTF_8,
|
104
103
|
code: response.code.to_i,
|
105
104
|
headers: response.headers
|
106
105
|
}
|
@@ -159,7 +158,6 @@ module RDF; module Util
|
|
159
158
|
# If a Location is returned, it defines the base resource for this file, not it's actual ending location
|
160
159
|
document_options = {
|
161
160
|
base_uri: RDF::URI(response["Location"] ? response["Location"] : base_uri),
|
162
|
-
charset: Encoding::UTF_8,
|
163
161
|
code: response.code.to_i,
|
164
162
|
content_type: response.content_type,
|
165
163
|
headers: response_headers
|
@@ -226,7 +224,6 @@ module RDF; module Util
|
|
226
224
|
# If a Location is returned, it defines the base resource for this file, not it's actual ending location
|
227
225
|
document_options = {
|
228
226
|
base_uri: RDF::URI(response.headers.fetch(:location, response.env.url)),
|
229
|
-
charset: Encoding::UTF_8,
|
230
227
|
code: response.status,
|
231
228
|
headers: response.headers
|
232
229
|
}
|
@@ -279,6 +276,8 @@ module RDF; module Util
|
|
279
276
|
# Adds Accept header based on available reader content types to allow
|
280
277
|
# for content negotiation based on available readers.
|
281
278
|
#
|
279
|
+
# Input received as non-unicode, is transformed to UTF-8. With Ruby >= 2.2, all UTF is normalized to [Unicode Normalization Form C (NFC)](http://unicode.org/reports/tr15/#Norm_Forms).
|
280
|
+
#
|
282
281
|
# HTTP resources may be retrieved via proxy using the `proxy` option. If `RestClient` is loaded, they will use the proxy globally by setting something like the following:
|
283
282
|
# `RestClient.proxy = "http://proxy.example.com/"`.
|
284
283
|
# When retrieving documents over HTTP(S), use the mechanism described in [Providing and Discovering URI Documentation](http://www.w3.org/2001/tag/awwsw/issue57/latest/) to pass the appropriate `base_uri` to the block or as the return.
|
@@ -328,11 +327,11 @@ module RDF; module Util
|
|
328
327
|
Kernel.open(filename_or_url, "r:utf-8", options) do |file|
|
329
328
|
document_options = {
|
330
329
|
base_uri: filename_or_url.to_s,
|
331
|
-
charset: file.external_encoding,
|
330
|
+
charset: file.external_encoding.to_s,
|
332
331
|
code: 200,
|
333
332
|
content_type: content_type,
|
334
333
|
last_modified:file.mtime,
|
335
|
-
headers: {
|
334
|
+
headers: {content_type: content_type, last_modified: file.mtime.xmlschema}
|
336
335
|
}
|
337
336
|
|
338
337
|
remote_document = RemoteDocument.new(file.read, document_options)
|
@@ -363,8 +362,8 @@ module RDF; module Util
|
|
363
362
|
# @return [String]
|
364
363
|
attr_reader :content_type
|
365
364
|
|
366
|
-
# Encoding of resource (from
|
367
|
-
# @return [
|
365
|
+
# Encoding of resource (from Content-Type), downcased. Also applied to content if it is UTF
|
366
|
+
# @return [String}]
|
368
367
|
attr_reader :charset
|
369
368
|
|
370
369
|
# Response code
|
@@ -381,7 +380,7 @@ module RDF; module Util
|
|
381
380
|
attr_reader :last_modified
|
382
381
|
|
383
382
|
# Raw headers from response
|
384
|
-
# @return [Hash{
|
383
|
+
# @return [Hash{Symbol => Object}]
|
385
384
|
attr_reader :headers
|
386
385
|
|
387
386
|
# Originally requested URL
|
@@ -390,40 +389,58 @@ module RDF; module Util
|
|
390
389
|
|
391
390
|
##
|
392
391
|
# Set content
|
393
|
-
# @param [String] body
|
392
|
+
# @param [String] body entity content of request.
|
394
393
|
def initialize(body, options = {})
|
395
|
-
super(body)
|
396
394
|
options.each do |key, value|
|
397
395
|
# de-quote charset
|
398
396
|
matchdata = value.match(/^["'](.*)["']$/.freeze) if key == "charset"
|
399
397
|
value = matchdata[1] if matchdata
|
398
|
+
value = value.downcase if value.is_a?(String)
|
400
399
|
instance_variable_set(:"@#{key}", value)
|
401
400
|
end
|
402
|
-
@headers
|
401
|
+
@headers = options.fetch(:headers, {})
|
402
|
+
@charset = options[:charset].to_s.downcase if options[:charset]
|
403
403
|
|
404
404
|
# Find Content-Type
|
405
|
-
if
|
406
|
-
|
405
|
+
if headers[:content_type]
|
406
|
+
ct, *params = headers[:content_type].split(';').map(&:strip)
|
407
|
+
@content_type ||= ct
|
407
408
|
|
408
409
|
# Find charset
|
409
410
|
params.each do |param|
|
410
411
|
p, v = param.split('=')
|
411
412
|
next unless p.downcase == 'charset'
|
412
|
-
@charset
|
413
|
+
@charset ||= v.sub(/^["']?(.*)["']?$/, '\1').downcase
|
413
414
|
end
|
414
415
|
end
|
415
416
|
|
416
417
|
@etag = headers[:etag]
|
417
418
|
@last_modified = DateTime.parse(headers[:last_modified]) if headers[:last_modified]
|
419
|
+
encoding = @charset ||= "utf-8"
|
420
|
+
|
421
|
+
unless encoding.start_with?("utf")
|
422
|
+
body.force_encoding(Encoding::UTF_8)
|
423
|
+
encoding = "utf-8"
|
424
|
+
end
|
418
425
|
|
419
|
-
|
426
|
+
# Make sure Unicode is in NFC
|
427
|
+
|
428
|
+
begin
|
429
|
+
body.unicode_normalize! unless !body.unicode_normalized?
|
430
|
+
rescue Encoding::CompatibilityError
|
431
|
+
# Oh, well ...
|
432
|
+
end if body.respond_to?(:unicode_normalized?)
|
433
|
+
|
434
|
+
super(body, "r:#{encoding}")
|
420
435
|
end
|
421
436
|
|
422
437
|
##
|
423
|
-
# Content
|
424
|
-
#
|
438
|
+
# Returns a list of encodings in Content-Encoding field as an array of strings.
|
439
|
+
#
|
440
|
+
# The encodings are downcased for canonicalization.
|
441
|
+
# @return [Array<String>]
|
425
442
|
def content_encoding
|
426
|
-
|
443
|
+
headers.fetch(:content_encoding, "").split(',').map(&:strip).map(&:downcase)
|
427
444
|
end
|
428
445
|
|
429
446
|
##
|