json-ld 3.0.2 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +1 -1
  3. data/README.md +90 -53
  4. data/UNLICENSE +1 -1
  5. data/VERSION +1 -1
  6. data/bin/jsonld +4 -4
  7. data/lib/json/ld.rb +27 -10
  8. data/lib/json/ld/api.rb +325 -96
  9. data/lib/json/ld/compact.rb +75 -27
  10. data/lib/json/ld/conneg.rb +188 -0
  11. data/lib/json/ld/context.rb +677 -292
  12. data/lib/json/ld/expand.rb +240 -75
  13. data/lib/json/ld/flatten.rb +5 -3
  14. data/lib/json/ld/format.rb +19 -19
  15. data/lib/json/ld/frame.rb +135 -85
  16. data/lib/json/ld/from_rdf.rb +44 -17
  17. data/lib/json/ld/html/nokogiri.rb +151 -0
  18. data/lib/json/ld/html/rexml.rb +186 -0
  19. data/lib/json/ld/reader.rb +25 -5
  20. data/lib/json/ld/resource.rb +2 -2
  21. data/lib/json/ld/streaming_writer.rb +3 -1
  22. data/lib/json/ld/to_rdf.rb +47 -17
  23. data/lib/json/ld/utils.rb +4 -2
  24. data/lib/json/ld/writer.rb +75 -14
  25. data/spec/api_spec.rb +13 -34
  26. data/spec/compact_spec.rb +968 -9
  27. data/spec/conneg_spec.rb +373 -0
  28. data/spec/context_spec.rb +447 -53
  29. data/spec/expand_spec.rb +1872 -416
  30. data/spec/flatten_spec.rb +434 -47
  31. data/spec/frame_spec.rb +979 -344
  32. data/spec/from_rdf_spec.rb +305 -5
  33. data/spec/spec_helper.rb +177 -0
  34. data/spec/streaming_writer_spec.rb +4 -4
  35. data/spec/suite_compact_spec.rb +2 -2
  36. data/spec/suite_expand_spec.rb +14 -2
  37. data/spec/suite_flatten_spec.rb +10 -2
  38. data/spec/suite_frame_spec.rb +3 -2
  39. data/spec/suite_from_rdf_spec.rb +2 -2
  40. data/spec/suite_helper.rb +55 -20
  41. data/spec/suite_html_spec.rb +22 -0
  42. data/spec/suite_http_spec.rb +35 -0
  43. data/spec/suite_remote_doc_spec.rb +2 -2
  44. data/spec/suite_to_rdf_spec.rb +14 -3
  45. data/spec/support/extensions.rb +5 -1
  46. data/spec/test-files/test-4-input.json +3 -3
  47. data/spec/test-files/test-5-input.json +2 -2
  48. data/spec/test-files/test-8-framed.json +14 -18
  49. data/spec/to_rdf_spec.rb +606 -16
  50. data/spec/writer_spec.rb +5 -5
  51. metadata +144 -88
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 970e177e4a776881601b87546f04cfad8c616b533d322a985f6ce588c5572c19
4
- data.tar.gz: 9d8388137478a542d0e42f143ed36d90efe27fa7ff649410af35d651fafce836
3
+ metadata.gz: a9fcb46a6c6bf33bfed92adcc8a751598cec5d6511e0450d38c9920bfdb696ce
4
+ data.tar.gz: 6fec905be8d5e0335149da757fef899d6e3fa948e852f74e9c88d0bff45801ae
5
5
  SHA512:
6
- metadata.gz: 70b363dafd7632225a082e0ea5113ceba39779e27593f25f18f78df62bed9a202b0f2a62627096186113888cbd5bf3583e4f33b85cf94d1bbf8ca10d1d02059f
7
- data.tar.gz: c6186a16b4fac554ff6893926f23c729fb1194ab3423e801d9d40915c978330a39c251645bb8775e1d46a94add2cf9f905c579f47085d2d42aafb1cd04971a61
6
+ metadata.gz: 1a7d07de17bc462f2f38fc24ae7c2a04a2c1ffd24d0a431626771259d59ffd6fe4b33b4ea8501add71d710e3c4501897454aada60ff6054fa9a12ef69cf488f3
7
+ data.tar.gz: 48da4933e6660ccd7cffe9cdcb08b576d883f1c744a889867b5e8619f9c3908fad00af02a739bc9af5c1abd7495d3cb9517d914ee12f7ac4f48acf94b5b9b637
data/AUTHORS CHANGED
@@ -1 +1 @@
1
- * Gregg Kellogg <gregg@kellogg-assoc.com>
1
+ * Gregg Kellogg <gregg@greggkellogg.net>
data/README.md CHANGED
@@ -2,17 +2,18 @@
2
2
 
3
3
  [JSON-LD][] reader/writer for [RDF.rb][RDF.rb] and fully conforming [JSON-LD API][] processor. Additionally this gem implements [JSON-LD Framing][].
4
4
 
5
- [![Gem Version](https://badge.fury.io/rb/json-ld.png)](http://badge.fury.io/rb/json-ld)
6
- [![Build Status](https://secure.travis-ci.org/ruby-rdf/json-ld.png?branch=master)](http://travis-ci.org/ruby-rdf/json-ld)
5
+ [![Gem Version](https://badge.fury.io/rb/json-ld.png)](https://badge.fury.io/rb/json-ld)
6
+ [![Build Status](https://secure.travis-ci.org/ruby-rdf/json-ld.png?branch=master)](https://travis-ci.org/ruby-rdf/json-ld)
7
7
  [![Coverage Status](https://coveralls.io/repos/ruby-rdf/json-ld/badge.svg)](https://coveralls.io/r/ruby-rdf/json-ld)
8
8
 
9
9
  ## Features
10
10
 
11
- JSON::LD parses and serializes [JSON-LD][] into [RDF][] and implements expansion, compaction and framing API interfaces.
11
+ JSON::LD parses and serializes [JSON-LD][] into [RDF][] and implements expansion, compaction and framing API interfaces. It also extracts JSON-LD from HTML.
12
12
 
13
13
  JSON::LD can now be used to create a _context_ from an RDFS/OWL definition, and optionally include a JSON-LD representation of the ontology itself. This is currently accessed through the `script/gen_context` script.
14
14
 
15
- If the [jsonlint][] gem is installed, it will be used when validating an input document.
15
+ * If the [jsonlint][] gem is installed, it will be used when validating an input document.
16
+ * If available, uses [Nokogiri][] and/or [Nokogumbo][] for parsing HTML, falls back to REXML otherwise.
16
17
 
17
18
  [Implementation Report](file.earl.html)
18
19
 
@@ -48,16 +49,16 @@ require 'json/ld'
48
49
 
49
50
  [{
50
51
  "http://xmlns.com/foaf/0.1/name": [{"@value"=>"Manu Sporny"}],
51
- "http://xmlns.com/foaf/0.1/homepage": [{"@value"=>"http://manu.sporny.org/"}],
52
- "http://xmlns.com/foaf/0.1/avatar": [{"@value": "http://twitter.com/account/profile_image/manusporny"}]
52
+ "http://xmlns.com/foaf/0.1/homepage": [{"@value"=>"https://manu.sporny.org/"}],
53
+ "http://xmlns.com/foaf/0.1/avatar": [{"@value": "https://twitter.com/account/profile_image/manusporny"}]
53
54
  }]
54
55
  ```
55
56
  ### Compact a Document
56
57
  ```ruby
57
58
  input = JSON.parse %([{
58
59
  "http://xmlns.com/foaf/0.1/name": ["Manu Sporny"],
59
- "http://xmlns.com/foaf/0.1/homepage": [{"@id": "http://manu.sporny.org/"}],
60
- "http://xmlns.com/foaf/0.1/avatar": [{"@id": "http://twitter.com/account/profile_image/manusporny"}]
60
+ "http://xmlns.com/foaf/0.1/homepage": [{"@id": "https://manu.sporny.org/"}],
61
+ "http://xmlns.com/foaf/0.1/avatar": [{"@id": "https://twitter.com/account/profile_image/manusporny"}]
61
62
  }])
62
63
 
63
64
  context = JSON.parse(%({
@@ -75,8 +76,8 @@ require 'json/ld'
75
76
  "homepage": {"@id": "http://xmlns.com/foaf/0.1/homepage", "@type": "@id"},
76
77
  "avatar": {"@id": "http://xmlns.com/foaf/0.1/avatar", "@type": "@id"}
77
78
  },
78
- "avatar": "http://twitter.com/account/profile_image/manusporny",
79
- "homepage": "http://manu.sporny.org/",
79
+ "avatar": "https://twitter.com/account/profile_image/manusporny",
80
+ "homepage": "https://manu.sporny.org/",
80
81
  "name": "Manu Sporny"
81
82
  }
82
83
  ```
@@ -167,7 +168,7 @@ require 'json/ld'
167
168
  ```ruby
168
169
  input = JSON.parse %({
169
170
  "@context": {
170
- "": "http://manu.sporny.org/",
171
+ "": "https://manu.sporny.org/",
171
172
  "foaf": "http://xmlns.com/foaf/0.1/"
172
173
  },
173
174
  "@id": "http://example.org/people#joebob",
@@ -192,7 +193,7 @@ require 'json/ld'
192
193
  input = RDF::Graph.new << RDF::Turtle::Reader.new(%(
193
194
  @prefix foaf: <http://xmlns.com/foaf/0.1/> .
194
195
 
195
- <http://manu.sporny.org/#me> a foaf:Person;
196
+ <https://manu.sporny.org/#me> a foaf:Person;
196
197
  foaf:knows [ a foaf:Person;
197
198
  foaf:name "Gregg Kellogg"];
198
199
  foaf:name "Manu Sporny" .
@@ -200,7 +201,7 @@ require 'json/ld'
200
201
 
201
202
  context = JSON.parse %({
202
203
  "@context": {
203
- "": "http://manu.sporny.org/",
204
+ "": "https://manu.sporny.org/",
204
205
  "foaf": "http://xmlns.com/foaf/0.1/"
205
206
  }
206
207
  })
@@ -217,7 +218,7 @@ require 'json/ld'
217
218
  "http://xmlns.com/foaf/0.1/name": [{"@value": "Gregg Kellogg"}]
218
219
  },
219
220
  {
220
- "@id": "http://manu.sporny.org/#me",
221
+ "@id": "https://manu.sporny.org/#me",
221
222
  "@type": ["http://xmlns.com/foaf/0.1/Person"],
222
223
  "http://xmlns.com/foaf/0.1/knows": [{"@id": "_:g70265766605380"}],
223
224
  "http://xmlns.com/foaf/0.1/name": [{"@value": "Manu Sporny"}]
@@ -225,22 +226,22 @@ require 'json/ld'
225
226
  ]
226
227
  ```
227
228
  ## Use a custom Document Loader
228
- In some cases, the built-in document loader {JSON::LD::API.documentLoader} is inadequate; for example, when using `http://schema.org` as a remote context, it will be re-loaded every time.
229
+ In some cases, the built-in document loader {JSON::LD::API.documentLoader} is inadequate; for example, when using `http://schema.org` as a remote context, it will be re-loaded every time (however, see [json-ld-preloaded](https://rubygems.org/gems/json-ld-preloaded)).
229
230
 
230
231
  All entries into the {JSON::LD::API} accept a `:documentLoader` option, which can be used to provide an alternative method to use when loading remote documents. For example:
231
232
  ```ruby
232
- def load_document_local(url, options={}, &block)
233
- if RDF::URI(url, canonicalize: true) == RDF::URI('http://schema.org/')
234
- remote_document = JSON::LD::API::RemoteDocument.new(url, File.read("etc/schema.org.jsonld"))
235
- return block_given? ? yield(remote_document) : remote_document
236
- else
237
- JSON::LD::API.documentLoader(url, options, &block)
238
- end
239
- end
233
+ def load_document_local(url, options={}, &block)
234
+ if RDF::URI(url, canonicalize: true) == RDF::URI('http://schema.org/')
235
+ remote_document = JSON::LD::API::RemoteDocument.new(url, File.read("etc/schema.org.jsonld"))
236
+ return block_given? ? yield(remote_document) : remote_document
237
+ else
238
+ JSON::LD::API.documentLoader(url, options, &block)
239
+ end
240
+ end
240
241
  ```
241
242
  Then, when performing something like expansion:
242
243
  ```ruby
243
- JSON::LD::API.expand(input, documentLoader: load_document_local)
244
+ JSON::LD::API.expand(input, documentLoader: load_document_local)
244
245
  ```
245
246
 
246
247
  ## Preloading contexts
@@ -433,19 +434,52 @@ Many JSON APIs separate properties from their entities using an intermediate obj
433
434
  ```
434
435
  In this way, nesting survives round-tripping through expansion, and framed output can include nested properties.
435
436
 
436
- ### Framing Updates
437
- The [JSON-LD Framing 1.1 Specification]() improves on previous un-released versions.
437
+ ## Sinatra/Rack support
438
+ JSON-LD 1.1 describes support for the _profile_ parameter to a media type in an HTTP ACCEPT header. This allows an HTTP request to specify the format (expanded/compacted/flattened/framed) along with a reference to a context or frame to use to format the returned document.
439
+
440
+ An HTTP header may be constructed as follows:
441
+
442
+ GET /ordinary-json-document.json HTTP/1.1
443
+ Host: example.com
444
+ Accept: application/ld+json;profile="http://www.w3.org/ns/json-ld#compacted http://conneg.example.com/context", application/ld+json
445
+
446
+ This tells a server that the top priority is to return JSON-LD compacted using a context at `http://conneg.example.com/context`, and if not available, to just return any form of JSON-LD.
447
+
448
+ The {JSON::LD::ContentNegotiation} class provides a [Rack][Rack] `call` method, and [Sinatra][Sinatra] `registered` class method to allow content-negotiation using such profile parameters. For example:
449
+
450
+ #!/usr/bin/env rackup
451
+ require 'sinatra/base'
452
+ require 'json/ld'
453
+
454
+ module My
455
+ class Application < Sinatra::Base
456
+ register JSON::LD::ContentNegotiation
457
+
458
+ get '/hello' do
459
+ [{
460
+ "http://example.org/input": [{
461
+ "@id": "http://example.com/g1",
462
+ "@graph": [{
463
+ "http://example.org/value": [{"@value": "x"}]
464
+ }]
465
+ }]
466
+ }])
467
+ end
468
+ end
469
+ end
470
+
471
+ run My::Application
472
+
473
+ The {JSON::LD::ContentNegotiation#call} method looks for a result which includes an object, with an acceptable `Accept` header and formats the result as JSON-LD, considering the profile parameters. This can be tested using something like the following:
474
+
475
+ $ rackup config.ru
476
+
477
+ $ curl -iH 'Accept: application/ld+json;profile="http://www.w3.org/ns/json-ld#compacted http://conneg.example.com/context"' http://localhost:9292/hello
438
478
 
439
- * [More Specific Frame matching](https://github.com/json-ld/json-ld.org/issues/110) – Allows framing to extend to elements of value objects, and objects are matched through recursive frame matching. `{}` is used as a wildcard, and `[]` as matching nothing.
440
- * [Graph framing](https://github.com/json-ld/json-ld.org/issues/118) – previously, only the merged graph can be framed, this update allows arbitrary graphs to be framed.
441
- * Use `@graph` in frame, matches the default graph, not the merged graph.
442
- * Use `@graph` in property value, causes the apropriatly named graph to be used for filling in values.
443
- * [Reverse properties](https://github.com/json-ld/json-ld.org/issues/311) – `@reverse` (or a property defined with `@reverse`) can cause matching values to be included, allowing a matched object to include reverse references to any objects referencing it.
444
- * [@omitDefault behavior](https://github.com/json-ld/json-ld.org/issues/389) – In addition to `true` and `false`, `@omitDefault` can take `@last`, `@always`, `@never`, and `@link`.
445
- * [multiple `@id` matching](https://github.com/json-ld/json-ld.org/issues/424) – A frame can match based on one or more specific object `@id` values.
479
+ See [Rack::LinkedData][] to do the same thing with an RDF Graph or Dataset as the source, rather than Ruby objects.
446
480
 
447
481
  ## Documentation
448
- Full documentation available on [RubyDoc](http://rubydoc.info/gems/json-ld/file/README.md)
482
+ Full documentation available on [RubyDoc](https://rubydoc.info/gems/json-ld/file/README.md)
449
483
 
450
484
  ## Differences from [JSON-LD API][]
451
485
  The specified JSON-LD API is based on a WebIDL definition implementing [Promises][] intended for use within a browser.
@@ -468,12 +502,12 @@ Note, the API method signatures differed in versions before 1.0, in that they al
468
502
  * {JSON::LD::Writer}
469
503
 
470
504
  ## Dependencies
471
- * [Ruby](http://ruby-lang.org/) (>= 2.2.2)
472
- * [RDF.rb](http://rubygems.org/gems/rdf) (~> 3.0)
473
- * [JSON](https://rubygems.org/gems/json) (>= 2.1)
505
+ * [Ruby](https://ruby-lang.org/) (>= 2.4)
506
+ * [RDF.rb](https://rubygems.org/gems/rdf) (~> 3.1)
507
+ * [JSON](https://rubygems.org/gems/json) (>= 2.2)
474
508
 
475
509
  ## Installation
476
- The recommended installation method is via [RubyGems](http://rubygems.org/).
510
+ The recommended installation method is via [RubyGems](https://rubygems.org/).
477
511
  To install the latest official release of the `JSON-LD` gem, do:
478
512
  ```bash
479
513
  % [sudo] gem install json-ld
@@ -484,10 +518,10 @@ To get a local working copy of the development repository, do:
484
518
  % git clone git://github.com/ruby-rdf/json-ld.git
485
519
  ```
486
520
  ## Mailing List
487
- * <http://lists.w3.org/Archives/Public/public-rdf-ruby/>
521
+ * <https://lists.w3.org/Archives/Public/public-rdf-ruby/>
488
522
 
489
523
  ## Author
490
- * [Gregg Kellogg](http://github.com/gkellogg) - <http://kellogg-assoc.com/>
524
+ * [Gregg Kellogg](https://github.com/gkellogg) - <https://greggkellogg.net/>
491
525
 
492
526
  ## Contributing
493
527
  * Do your best to adhere to the existing coding conventions and idioms.
@@ -506,17 +540,20 @@ License
506
540
  -------
507
541
 
508
542
  This is free and unencumbered public domain software. For more information,
509
- see <http://unlicense.org/> or the accompanying {file:UNLICENSE} file.
510
-
511
- [Ruby]: http://ruby-lang.org/
512
- [RDF]: http://www.w3.org/RDF/
513
- [YARD]: http://yardoc.org/
514
- [YARD-GS]: http://rubydoc.info/docs/yard/file/docs/GettingStarted.md
515
- [PDD]: http://lists.w3.org/Archives/Public/public-rdf-ruby/2010May/0013.html
516
- [RDF.rb]: http://rubygems.org/gems/rdf
517
- [Backports]: http://rubygems.org/gems/backports
518
- [JSON-LD]: http://www.w3.org/TR/json-ld/ "JSON-LD 1.0"
519
- [JSON-LD API]: http://www.w3.org/TR/json-ld-api/ "JSON-LD 1.0 Processing Algorithms and API"
520
- [JSON-LD Framing]: http://json-ld.org/spec/latest/json-ld-framing/ "JSON-LD Framing 1.0"
521
- [Promises]: http://dom.spec.whatwg.org/#promises
543
+ see <https://unlicense.org/> or the accompanying {file:UNLICENSE} file.
544
+
545
+ [Ruby]: https://ruby-lang.org/
546
+ [RDF]: https://www.w3.org/RDF/
547
+ [YARD]: https://yardoc.org/
548
+ [YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md
549
+ [PDD]: https://lists.w3.org/Archives/Public/public-rdf-ruby/2010May/0013.html
550
+ [RDF.rb]: https://rubygems.org/gems/rdf
551
+ [Rack::LinkedData]: https://rubygems.org/gems/rack-linkeddata
552
+ [Backports]: https://rubygems.org/gems/backports
553
+ [JSON-LD]: https://www.w3.org/TR/json-ld11/ "JSON-LD 1.1"
554
+ [JSON-LD API]: https://www.w3.org/TR/json-ld11-api/ "JSON-LD 1.1 Processing Algorithms and API"
555
+ [JSON-LD Framing]: https://www.w3.org/TR/json-ld11-framing/ "JSON-LD Framing 1.1"
556
+ [Promises]: https://dom.spec.whatwg.org/#promises
522
557
  [jsonlint]: https://rubygems.org/gems/jsonlint
558
+ [Sinatra]: https://www.sinatrarb.com/
559
+ [Rack]: https://rack.github.com/
data/UNLICENSE CHANGED
@@ -21,4 +21,4 @@ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
21
  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
22
  OTHER DEALINGS IN THE SOFTWARE.
23
23
 
24
- For more information, please refer to <http://unlicense.org/>
24
+ For more information, please refer to <https://unlicense.org/>
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.2
1
+ 3.1.0
data/bin/jsonld CHANGED
@@ -168,14 +168,14 @@ opts.each do |opt, arg|
168
168
  when '--help' then usage
169
169
  when '--embed'
170
170
  case arg
171
- when '@always', '@never', '@link', '@last'
171
+ when '@always', '@never', '@link', '@last', '@first'
172
172
  options[:embed] = arg
173
173
  when 'true'
174
- options[:embed] = true
174
+ options[:embed] = '@never'
175
175
  when 'false'
176
- options[:embed] = false
176
+ options[:embed] = '@first'
177
177
  else
178
- STDERR.puts "--embed option takes one of '@always', '@never', '@link', '@last', true, or false"
178
+ STDERR.puts "--embed option takes one of @always, @never, @link, @first, or @last"
179
179
  exit(1)
180
180
  end
181
181
  end
@@ -29,6 +29,7 @@ module JSON
29
29
  require 'json/ld/format'
30
30
  require 'json/ld/utils'
31
31
  autoload :API, 'json/ld/api'
32
+ autoload :ContentNegotiation, 'json/ld/conneg'
32
33
  autoload :Context, 'json/ld/context'
33
34
  autoload :Normalize, 'json/ld/normalize'
34
35
  autoload :Reader, 'json/ld/reader'
@@ -36,27 +37,35 @@ module JSON
36
37
  autoload :VERSION, 'json/ld/version'
37
38
  autoload :Writer, 'json/ld/writer'
38
39
 
39
- # Initial context
40
- # @see http://json-ld.org/spec/latest/json-ld-api/#appendix-b
41
- INITIAL_CONTEXT = {
42
- RDF.type.to_s => {"@type" => "@id"}
43
- }.freeze
40
+ # JSON-LD profiles
41
+ JSON_LD_NS = "http://www.w3.org/ns/json-ld#"
42
+ PROFILES = %w(expanded compacted flattened framed).map {|p| JSON_LD_NS + p}.freeze
43
+
44
+ # Default context when compacting without one being specified
45
+ DEFAULT_CONTEXT = "http://schema.org"
44
46
 
45
47
  KEYWORDS = Set.new(%w(
46
48
  @base
47
49
  @container
48
50
  @context
49
51
  @default
52
+ @direction
50
53
  @embed
51
54
  @explicit
55
+ @json
52
56
  @id
57
+ @included
53
58
  @index
59
+ @first
54
60
  @graph
61
+ @import
55
62
  @language
56
63
  @list
57
64
  @nest
58
65
  @none
59
66
  @omitDefault
67
+ @propagate
68
+ @protected
60
69
  @requireAll
61
70
  @reverse
62
71
  @set
@@ -106,11 +115,16 @@ module JSON
106
115
  class CyclicIRIMapping < JsonLdError; @code = "cyclic IRI mapping"; end
107
116
  class InvalidBaseIRI < JsonLdError; @code = "invalid base IRI"; end
108
117
  class InvalidContainerMapping < JsonLdError; @code = "invalid container mapping"; end
118
+ class InvalidContextMember < JsonLdError; @code = "invalid context member"; end
119
+ class InvalidContextNullification < JsonLdError; @code = "invalid context nullification"; end
109
120
  class InvalidDefaultLanguage < JsonLdError; @code = "invalid default language"; end
110
121
  class InvalidIdValue < JsonLdError; @code = "invalid @id value"; end
111
122
  class InvalidIndexValue < JsonLdError; @code = "invalid @index value"; end
112
123
  class InvalidVersionValue < JsonLdError; @code = "invalid @version value"; end
124
+ class InvalidImportValue < JsonLdError; @code = "invalid @import value"; end
125
+ class InvalidIncludedValue < JsonLdError; @code = "invalid @included value"; end
113
126
  class InvalidIRIMapping < JsonLdError; @code = "invalid IRI mapping"; end
127
+ class InvalidJsonLiteral < JsonLdError; @code = "invalid JSON literal"; end
114
128
  class InvalidKeywordAlias < JsonLdError; @code = "invalid keyword alias"; end
115
129
  class InvalidLanguageMapping < JsonLdError; @code = "invalid language mapping"; end
116
130
  class InvalidLanguageMapValue < JsonLdError; @code = "invalid language map value"; end
@@ -119,31 +133,34 @@ module JSON
119
133
  class InvalidLocalContext < JsonLdError; @code = "invalid local context"; end
120
134
  class InvalidNestValue < JsonLdError; @code = "invalid @nest value"; end
121
135
  class InvalidPrefixValue < JsonLdError; @code = "invalid @prefix value"; end
136
+ class InvalidPropagateValue < JsonLdError; @code = "invalid @propagate value"; end
122
137
  class InvalidRemoteContext < JsonLdError; @code = "invalid remote context"; end
123
138
  class InvalidReverseProperty < JsonLdError; @code = "invalid reverse property"; end
124
139
  class InvalidReversePropertyMap < JsonLdError; @code = "invalid reverse property map"; end
125
140
  class InvalidReversePropertyValue < JsonLdError; @code = "invalid reverse property value"; end
126
141
  class InvalidReverseValue < JsonLdError; @code = "invalid @reverse value"; end
127
142
  class InvalidScopedContext < JsonLdError; @code = "invalid scoped context"; end
143
+ class InvalidScriptElement < JsonLdError; @code = "invalid script element"; end
128
144
  class InvalidSetOrListObject < JsonLdError; @code = "invalid set or list object"; end
129
145
  class InvalidTermDefinition < JsonLdError; @code = "invalid term definition"; end
146
+ class InvalidBaseDirection < JsonLdError; @code = "invalid base direction"; end
130
147
  class InvalidTypedValue < JsonLdError; @code = "invalid typed value"; end
131
148
  class InvalidTypeMapping < JsonLdError; @code = "invalid type mapping"; end
132
149
  class InvalidTypeValue < JsonLdError; @code = "invalid type value"; end
133
150
  class InvalidValueObject < JsonLdError; @code = "invalid value object"; end
134
151
  class InvalidValueObjectValue < JsonLdError; @code = "invalid value object value"; end
135
152
  class InvalidVocabMapping < JsonLdError; @code = "invalid vocab mapping"; end
153
+ class IRIConfusedWithPrefix < JsonLdError; @code = "IRI confused with prefix"; end
136
154
  class KeywordRedefinition < JsonLdError; @code = "keyword redefinition"; end
137
155
  class LoadingDocumentFailed < JsonLdError; @code = "loading document failed"; end
138
156
  class LoadingRemoteContextFailed < JsonLdError; @code = "loading remote context failed"; end
139
157
  class ContextOverflow < JsonLdError; @code = "maximum number of @context URLs exceeded"; end
158
+ class MissingIncludedReferent < JsonLdError; @code = "missing @included referent"; end
140
159
  class MultipleContextLinkHeaders < JsonLdError; @code = "multiple context link headers"; end
160
+ class ProtectedTermRedefinition < JsonLdError; @code = "protected term redefinition"; end
141
161
  class ProcessingModeConflict < JsonLdError; @code = "processing mode conflict"; end
142
- class RecursiveContextInclusion < JsonLdError; @code = "recursive context inclusion"; end
143
- class InvalidFrame < JsonLdError
144
- class MultipleEmbeds < InvalidFrame; end
145
- class Syntax < InvalidFrame; end
146
- end
162
+ class InvalidFrame < JsonLdError; @code = "invalid frame"; end
163
+ class InvalidEmbedValue < InvalidFrame; @code = "invalid @embed value"; end
147
164
  end
148
165
  end
149
166
  end
@@ -1,6 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  # frozen_string_literal: true
3
3
  require 'openssl'
4
+ require 'cgi'
4
5
  require 'json/ld/expand'
5
6
  require 'json/ld/compact'
6
7
  require 'json/ld/flatten'
@@ -21,7 +22,7 @@ module JSON::LD
21
22
  #
22
23
  # Note that the API method signatures are somewhat different than what is specified, as the use of Futures and explicit callback parameters is not as relevant for Ruby-based interfaces.
23
24
  #
24
- # @see http://json-ld.org/spec/latest/json-ld-api/#the-application-programming-interface
25
+ # @see https://www.w3.org/TR/json-ld11-api/#the-application-programming-interface
25
26
  # @author [Gregg Kellogg](http://greggkellogg.net/)
26
27
  class API
27
28
  include Expand
@@ -34,9 +35,15 @@ module JSON::LD
34
35
 
35
36
  # Options used for open_file
36
37
  OPEN_OPTS = {
37
- headers: {"Accept" => "application/ld+json, application/json"}
38
+ headers: {"Accept" => "application/ld+json, text/html;q=0.8, application/json;q=0.5"}
38
39
  }
39
40
 
41
+ # The following constants are used to reduce object allocations
42
+ LINK_REL_CONTEXT = %w(rel http://www.w3.org/ns/json-ld#context).freeze
43
+ LINK_REL_ALTERNATE = %w(rel alternate).freeze
44
+ LINK_TYPE_JSONLD = %w(type application/ld+json).freeze
45
+ JSON_LD_PROCESSING_MODES = %w(json-ld-1.0 json-ld-1.1).freeze
46
+
40
47
  # Current input
41
48
  # @!attribute [rw] input
42
49
  # @return [String, #read, Hash, Array]
@@ -67,12 +74,18 @@ module JSON::LD
67
74
  # Creates document relative IRIs when compacting, if `true`, otherwise leaves expanded.
68
75
  # @option options [Proc] :documentLoader
69
76
  # The callback of the loader to be used to retrieve remote documents and contexts. If specified, it must be used to retrieve remote documents and contexts; otherwise, if not specified, the processor's built-in loader must be used. See {documentLoader} for the method signature.
77
+ # @option options [Boolean] :lowercaseLanguage
78
+ # By default, language tags are left as is. To normalize to lowercase, set this option to `true`.
70
79
  # @option options [String, #read, Hash, Array, JSON::LD::Context] :expandContext
71
80
  # A context that is used to initialize the active context when expanding a document.
81
+ # @option options [Boolean] :extractAllScripts
82
+ # If set, when given an HTML input without a fragment identifier, extracts all `script` elements with type `application/ld+json` into an array during expansion.
72
83
  # @option options [Boolean, String, RDF::URI] :flatten
73
84
  # If set to a value that is not `false`, the JSON-LD processor must modify the output of the Compaction Algorithm or the Expansion Algorithm by coalescing all properties associated with each subject via the Flattening Algorithm. The value of `flatten must` be either an _IRI_ value representing the name of the graph to flatten, or `true`. If the value is `true`, then the first graph encountered in the input document is selected and flattened.
74
85
  # @option options [String] :language
75
86
  # When set, this has the effect of inserting a context definition with `@language` set to the associated value, creating a default language for interpreting string values.
87
+ # @option options [Symbol] :library
88
+ # One of :nokogiri or :rexml. If nil/unspecified uses :nokogiri if available, :rexml otherwise.
76
89
  # @option options [String] :processingMode
77
90
  # Processing mode, json-ld-1.0 or json-ld-1.1.
78
91
  # If `processingMode` is not specified, a mode of `json-ld-1.0` or `json-ld-1.1` is set, the context used for `expansion` or `compaction`.
@@ -91,7 +104,7 @@ module JSON::LD
91
104
  @options = {
92
105
  compactArrays: true,
93
106
  ordered: false,
94
- documentLoader: self.class.method(:documentLoader)
107
+ extractAllScripts: false,
95
108
  }.merge(options)
96
109
  @namer = unique_bnodes ? BlankNodeUniqer.new : (rename_bnodes ? BlankNodeNamer.new("b") : BlankNodeMapper.new)
97
110
 
@@ -100,41 +113,24 @@ module JSON::LD
100
113
 
101
114
  @value = case input
102
115
  when Array, Hash then input.dup
103
- when IO, StringIO
104
- @options = {base: input.base_uri}.merge(@options) if input.respond_to?(:base_uri)
105
-
106
- # if input impelements #links, attempt to get a contextUrl from that link
107
- content_type = input.respond_to?(:content_type) ? input.content_type : "application/json"
108
- context_ref = if content_type.start_with?('application/json') && input.respond_to?(:links)
109
- link = input.links.find_link(%w(rel http://www.w3.org/ns/json-ld#context))
110
- link.href if link
111
- end
112
-
113
- validate_input(input) if options[:validate]
114
-
115
- MultiJson.load(input.read, options)
116
- when String
117
- remote_doc = @options[:documentLoader].call(input, @options)
116
+ when IO, StringIO, String
117
+ remote_doc = self.class.loadRemoteDocument(input, **@options)
118
118
 
119
119
  context_ref = remote_doc.contextUrl
120
- @options = {base: remote_doc.documentUrl}.merge(@options) unless @options[:no_default_base]
120
+ @options[:base] = remote_doc.documentUrl if remote_doc.documentUrl && !@options[:no_default_base]
121
121
 
122
122
  case remote_doc.document
123
123
  when String
124
- validate_input(remote_doc.document) if options[:validate]
125
- MultiJson.load(remote_doc.document, options)
124
+ MultiJson.load(remote_doc.document, **options)
126
125
  else
126
+ # Already parsed
127
127
  remote_doc.document
128
128
  end
129
129
  end
130
130
 
131
131
  # If not provided, first use context from document, or from a Link header
132
- context ||= (@value['@context'] if @value.is_a?(Hash)) || context_ref
133
- @context = Context.parse(context || {}, @options)
134
-
135
- # If not set explicitly, the context figures out the processing mode
136
- @options[:processingMode] ||= @context.processingMode || "json-ld-1.0"
137
- @options[:validate] ||= %w(json-ld-1.0 json-ld-1.1).include?(@options[:processingMode])
132
+ context ||= context_ref || {}
133
+ @context = Context.parse(context || {}, **@options)
138
134
 
139
135
  if block_given?
140
136
  case block.arity
@@ -165,10 +161,10 @@ module JSON::LD
165
161
  # @yieldreturn [Object] returned object
166
162
  # @return [Object, Array<Hash>]
167
163
  # If a block is given, the result of evaluating the block is returned, otherwise, the expanded JSON-LD document
168
- # @see http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm
164
+ # @see https://www.w3.org/TR/json-ld11-api/#expansion-algorithm
169
165
  def self.expand(input, framing: false, **options, &block)
170
166
  result, doc_base = nil
171
- API.new(input, options[:expandContext], options) do
167
+ API.new(input, options[:expandContext], **options) do
172
168
  result = self.expand(self.value, nil, self.context,
173
169
  ordered: @options[:ordered],
174
170
  framing: framing)
@@ -214,14 +210,14 @@ module JSON::LD
214
210
  # @return [Object, Hash]
215
211
  # If a block is given, the result of evaluating the block is returned, otherwise, the compacted JSON-LD document
216
212
  # @raise [JsonLdError]
217
- # @see http://json-ld.org/spec/latest/json-ld-api/#compaction-algorithm
213
+ # @see https://www.w3.org/TR/json-ld11-api/#compaction-algorithm
218
214
  def self.compact(input, context, expanded: false, **options)
219
215
  result = nil
220
216
  options = {compactToRelative: true}.merge(options)
221
217
 
222
218
  # 1) Perform the Expansion Algorithm on the JSON-LD input.
223
219
  # This removes any existing context to allow the given context to be cleanly applied.
224
- expanded_input = expanded ? input : API.expand(input, options.merge(ordered: false)) do |res, base_iri|
220
+ expanded_input = expanded ? input : API.expand(input, ordered: false, **options) do |res, base_iri|
225
221
  options[:base] ||= base_iri if options[:compactToRelative]
226
222
  res
227
223
  end
@@ -259,13 +255,16 @@ module JSON::LD
259
255
  # @yieldreturn [Object] returned object
260
256
  # @return [Object, Hash]
261
257
  # If a block is given, the result of evaluating the block is returned, otherwise, the flattened JSON-LD document
262
- # @see http://json-ld.org/spec/latest/json-ld-api/#framing-algorithm
258
+ # @see https://www.w3.org/TR/json-ld11-api/#framing-algorithm
263
259
  def self.flatten(input, context, expanded: false, **options)
264
260
  flattened = []
265
- options = {compactToRelative: true}.merge(options)
261
+ options = {
262
+ compactToRelative: true,
263
+ extractAllScripts: true,
264
+ }.merge(options)
266
265
 
267
266
  # Expand input to simplify processing
268
- expanded_input = expanded ? input : API.expand(input, options) do |result, base_iri|
267
+ expanded_input = expanded ? input : API.expand(input, **options) do |result, base_iri|
269
268
  options[:base] ||= base_iri if options[:compactToRelative]
270
269
  result
271
270
  end
@@ -314,7 +313,7 @@ module JSON::LD
314
313
  # @param [String, #read, Hash, Array] frame
315
314
  # The frame to use when re-arranging the data.
316
315
  # @option options (see #initialize)
317
- # @option options ['@last', '@always', '@never', '@link'] :embed ('@last')
316
+ # @option options ['@always', '@first', '@last', '@link', '@once', '@never'] :embed ('@last')
318
317
  # a flag specifying that objects should be directly embedded in the output, instead of being referred to by their IRI.
319
318
  # @option options [Boolean] :explicit (false)
320
319
  # a flag specifying that for properties to be included in the output, they must be explicitly declared in the framing context.
@@ -323,6 +322,7 @@ module JSON::LD
323
322
  # @option options [Boolean] :omitDefault (false)
324
323
  # a flag specifying that properties that are missing from the JSON-LD input should be omitted from the output.
325
324
  # @option options [Boolean] :expanded Input is already expanded
325
+ # @option options [Boolean] :pruneBlankNodeIdentifiers (true) removes blank node identifiers that are only used once.
326
326
  # @option options [Boolean] :omitGraph does not use `@graph` at top level unless necessary to describe multiple objects, defaults to `true` if processingMode is 1.1, otherwise `false`.
327
327
  # @yield jsonld
328
328
  # @yieldparam [Hash] jsonld
@@ -331,18 +331,17 @@ module JSON::LD
331
331
  # @return [Object, Hash]
332
332
  # If a block is given, the result of evaluating the block is returned, otherwise, the framed JSON-LD document
333
333
  # @raise [InvalidFrame]
334
- # @see http://json-ld.org/spec/latest/json-ld-api/#framing-algorithm
334
+ # @see https://www.w3.org/TR/json-ld11-api/#framing-algorithm
335
335
  def self.frame(input, frame, expanded: false, **options)
336
336
  result = nil
337
337
  options = {
338
338
  base: (input if input.is_a?(String)),
339
339
  compactArrays: true,
340
340
  compactToRelative: true,
341
- embed: '@last',
341
+ embed: '@once',
342
342
  explicit: false,
343
- requireAll: true,
343
+ requireAll: false,
344
344
  omitDefault: false,
345
- documentLoader: method(:documentLoader)
346
345
  }.merge(options)
347
346
 
348
347
  framing_state = {
@@ -350,37 +349,46 @@ module JSON::LD
350
349
  graphStack: [],
351
350
  subjectStack: [],
352
351
  link: {},
352
+ embedded: false # False at the top-level
353
353
  }
354
354
 
355
355
  # de-reference frame to create the framing object
356
356
  frame = case frame
357
357
  when Hash then frame.dup
358
- when IO, StringIO then MultiJson.load(frame.read)
359
- when String
360
- remote_doc = options[:documentLoader].call(frame)
361
- case remote_doc.document
362
- when String then MultiJson.load(remote_doc.document)
363
- else remote_doc.document
358
+ when IO, StringIO, String
359
+ remote_doc = loadRemoteDocument(frame,
360
+ profile: 'http://www.w3.org/ns/json-ld#frame',
361
+ requestProfile: 'http://www.w3.org/ns/json-ld#frame',
362
+ **options)
363
+ if remote_doc.document.is_a?(String)
364
+ MultiJson.load(remote_doc.document)
365
+ else
366
+ remote_doc.document
364
367
  end
365
368
  end
366
369
 
367
370
  # Expand input to simplify processing
368
- expanded_input = expanded ? input : API.expand(input, options.merge(ordered: false)) do |res, base_iri|
371
+ expanded_input = expanded ? input : API.expand(input, ordered: false, **options) do |res, base_iri|
369
372
  options[:base] ||= base_iri if options[:compactToRelative]
370
373
  res
371
374
  end
372
375
 
373
376
  # Expand frame to simplify processing
374
- expanded_frame = API.expand(frame, options.merge(framing: true, ordered: false))
377
+ expanded_frame = API.expand(frame, framing: true, ordered: false, **options)
375
378
 
376
379
  # Initialize input using frame as context
377
380
  API.new(expanded_input, frame['@context'], no_default_base: true, **options) do
378
381
  log_debug(".frame") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
379
382
  log_debug(".frame") {"expanded frame: #{expanded_frame.to_json(JSON_STATE) rescue 'malformed json'}"}
380
383
 
384
+ if %w(@first @last).include?(options[:embed]) && context.processingMode('json-ld-1.1')
385
+ raise JSON::LD::JsonLdError::InvalidEmbedValue, "#{options[:embed]} is not a valid value of @embed in 1.1 mode" if @options[:validate]
386
+ warn "[DEPRECATION] #{options[:embed]} is not a valid value of @embed in 1.1 mode.\n"
387
+ end
388
+
381
389
  # Set omitGraph option, if not present, based on processingMode
382
390
  unless options.has_key?(:omitGraph)
383
- options[:omitGraph] = @options[:processingMode] != 'json-ld-1.0'
391
+ options[:omitGraph] = context.processingMode('json-ld-1.1')
384
392
  end
385
393
 
386
394
  # Get framing nodes from expanded input, replacing Blank Node identifiers as necessary
@@ -402,16 +410,26 @@ module JSON::LD
402
410
  result = []
403
411
  frame(framing_state, framing_state[:subjects].keys.opt_sort(ordered: @options[:ordered]), (expanded_frame.first || {}), parent: result, **options)
404
412
 
413
+ # Default to based on processinMode
414
+ if !options.has_key?(:pruneBlankNodeIdentifiers)
415
+ options[:pruneBlankNodeIdentifiers] = context.processingMode('json-ld-1.1')
416
+ end
417
+
405
418
  # Count blank node identifiers used in the document, if pruning
406
- unless @options[:processingMode] == 'json-ld-1.0'
419
+ if options[:pruneBlankNodeIdentifiers]
407
420
  bnodes_to_clear = count_blank_node_identifiers(result).collect {|k, v| k if v == 1}.compact
408
421
  result = prune_bnodes(result, bnodes_to_clear)
409
422
  end
410
423
 
411
- # Initalize context from frame
412
- @context = @context.parse(frame['@context'])
424
+ # Replace values with `@preserve` with the content of its entry.
425
+ result = cleanup_preserve(result)
426
+ log_debug(".frame") {"expanded result: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
427
+
413
428
  # Compact result
414
429
  compacted = compact(result, ordered: @options[:ordered])
430
+
431
+ # @replace `@null` with nil, compacting arrays
432
+ compacted = cleanup_null(compacted)
415
433
  compacted = [compacted] unless options[:omitGraph] || compacted.is_a?(Array)
416
434
 
417
435
  # Add the given context to the output
@@ -422,7 +440,7 @@ module JSON::LD
422
440
  context.serialize.merge({kwgraph => compacted})
423
441
  end
424
442
  log_debug(".frame") {"after compact: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
425
- result = cleanup_preserve(result)
443
+ result
426
444
  end
427
445
 
428
446
  block_given? ? yield(result) : result
@@ -445,16 +463,20 @@ module JSON::LD
445
463
  unless block_given?
446
464
  results = []
447
465
  results.extend(RDF::Enumerable)
448
- self.toRdf(input, options) do |stmt|
466
+ self.toRdf(input, **options) do |stmt|
449
467
  results << stmt
450
468
  end
451
469
  return results
452
470
  end
453
471
 
472
+ options = {
473
+ extractAllScripts: true,
474
+ }.merge(options)
475
+
454
476
  # Expand input to simplify processing
455
- expanded_input = expanded ? input : API.expand(input, options.merge(ordered: false))
477
+ expanded_input = expanded ? input : API.expand(input, ordered: false, **options)
456
478
 
457
- API.new(expanded_input, nil, options) do
479
+ API.new(expanded_input, nil, **options) do
458
480
  # 1) Perform the Expansion Algorithm on the JSON-LD input.
459
481
  # This removes any existing context to allow the given context to be cleanly applied.
460
482
  log_debug(".toRdf") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
@@ -496,7 +518,7 @@ module JSON::LD
496
518
  def self.fromRdf(input, useRdfType: false, useNativeTypes: false, **options, &block)
497
519
  result = nil
498
520
 
499
- API.new(nil, nil, options) do
521
+ API.new(nil, nil, **options) do
500
522
  result = from_statements(input,
501
523
  useRdfType: useRdfType,
502
524
  useNativeTypes: useNativeTypes,
@@ -507,62 +529,253 @@ module JSON::LD
507
529
  end
508
530
 
509
531
  ##
510
- # Default document loader.
532
+ # Uses built-in or provided documentLoader to retrieve a parsed document.
533
+ #
511
534
  # @param [RDF::URI, String] url
512
- # @param [Hash<Symbol => Object>] options
513
- # @option options [Boolean] :validate
535
+ # @param [Boolean] extractAllScripts
536
+ # If set to `true`, when extracting JSON-LD script elements from HTML, unless a specific fragment identifier is targeted, extracts all encountered JSON-LD script elements using an array form, if necessary.
537
+ # @param [String] profile
538
+ # When the resulting `contentType` is `text/html`, this option determines the profile to use for selecting a JSON-LD script elements.
539
+ # @param [String] requestProfile
540
+ # One or more IRIs to use in the request as a profile parameter.
541
+ # @param [Boolean] validate
514
542
  # Allow only appropriate content types
543
+ # @param [String, RDF::URI] base
544
+ # Location to use as documentUrl instead of `url`.
545
+ # @param [Hash<Symbol => Object>] options
515
546
  # @yield remote_document
516
- # @yieldparam [RemoteDocument] remote_document
547
+ # @yieldparam [RemoteDocumentRemoteDocument, RDF::Util::File::RemoteDocument] remote_document
517
548
  # @yieldreturn [Object] returned object
518
549
  # @return [Object, RemoteDocument]
519
550
  # If a block is given, the result of evaluating the block is returned, otherwise, the retrieved remote document and context information unless block given
520
551
  # @raise [JsonLdError]
521
- def self.documentLoader(url, validate: false, **options)
552
+ def self.loadRemoteDocument(url,
553
+ extractAllScripts: false,
554
+ profile: nil,
555
+ requestProfile: nil,
556
+ validate: false,
557
+ base: nil,
558
+ **options)
559
+ documentLoader = options.fetch(:documentLoader, self.method(:documentLoader))
522
560
  options = OPEN_OPTS.merge(options)
523
- RDF::Util::File.open_file(url, options) do |remote_doc|
524
- content_type = remote_doc.content_type if remote_doc.respond_to?(:content_type)
525
- # If the passed input is a DOMString representing the IRI of a remote document, dereference it. If the retrieved document's content type is neither application/json, nor application/ld+json, nor any other media type using a +json suffix as defined in [RFC6839], reject the promise passing an loading document failed error.
526
- if content_type && validate
527
- main, sub = content_type.split("/")
528
- raise JSON::LD::JsonLdError::LoadingDocumentFailed, "url: #{url}, content_type: #{content_type}" if
529
- main != 'application' ||
530
- sub !~ /^(.*\+)?json$/
561
+ if requestProfile
562
+ # Add any request profile
563
+ options[:headers]['Accept'] = options[:headers]['Accept'].sub('application/ld+json,', "application/ld+json;profile=#{requestProfile}, application/ld+json;q=0.9,")
564
+ end
565
+ documentLoader.call(url, **options) do |remote_doc|
566
+ case remote_doc
567
+ when RDF::Util::File::RemoteDocument
568
+ # Convert to RemoteDocument
569
+ context_url = if remote_doc.content_type != 'application/ld+json' &&
570
+ (remote_doc.content_type == 'application/json' ||
571
+ remote_doc.content_type.to_s.match?(%r(application/\w+\+json)))
572
+ # Get context link(s)
573
+ # Note, we can't simply use #find_link, as we need to detect multiple
574
+ links = remote_doc.links.links.select do |link|
575
+ link.attr_pairs.include?(LINK_REL_CONTEXT)
576
+ end
577
+ raise JSON::LD::JsonLdError::MultipleContextLinkHeaders,
578
+ "expected at most 1 Link header with rel=jsonld:context, got #{links.length}" if links.length > 1
579
+ Array(links.first).first
580
+ end
581
+
582
+ # If content-type is not application/ld+json, nor any other +json and a link with rel=alternate and type='application/ld+json' is found, use that instead
583
+ alternate = !remote_doc.content_type.match?(%r(application/(\w*\+)?json)) && remote_doc.links.links.detect do |link|
584
+ link.attr_pairs.include?(LINK_REL_ALTERNATE) &&
585
+ link.attr_pairs.include?(LINK_TYPE_JSONLD)
586
+ end
587
+
588
+ remote_doc = if alternate
589
+ # Load alternate relative to URL
590
+ loadRemoteDocument(RDF::URI(url).join(alternate.href),
591
+ extractAllScripts: extractAllScripts,
592
+ profile: profile,
593
+ requestProfile: requestProfile,
594
+ validate: validate,
595
+ base: base,
596
+ **options)
597
+ else
598
+ RemoteDocument.new(remote_doc.read,
599
+ documentUrl: remote_doc.base_uri,
600
+ contentType: remote_doc.content_type,
601
+ contextUrl: context_url)
602
+ end
603
+ when RemoteDocument
604
+ # Pass through
605
+ else
606
+ raise JSON::LD::JsonLdError::LoadingDocumentFailed, "unknown result from documentLoader: #{remote_doc.class}"
531
607
  end
532
608
 
533
- # If the input has been retrieved, the response has an HTTP Link Header [RFC5988] using the http://www.w3.org/ns/json-ld#context link relation and a content type of application/json or any media type with a +json suffix as defined in [RFC6839] except application/ld+json, update the active context using the Context Processing algorithm, passing the context referenced in the HTTP Link Header as local context. The HTTP Link Header is ignored for documents served as application/ld+json If multiple HTTP Link Headers using the http://www.w3.org/ns/json-ld#context link relation are found, the promise is rejected with a JsonLdError whose code is set to multiple context link headers and processing is terminated.
534
- contextUrl = unless content_type.nil? || content_type.start_with?("application/ld+json")
535
- # Get context link(s)
536
- # Note, we can't simply use #find_link, as we need to detect multiple
537
- links = remote_doc.links.links.select do |link|
538
- link.attr_pairs.include?(%w(rel http://www.w3.org/ns/json-ld#context))
609
+ # Use specified document location
610
+ remote_doc.documentUrl = base if base
611
+
612
+ # Parse any HTML
613
+ if remote_doc.document.is_a?(String)
614
+ remote_doc.document = case remote_doc.contentType
615
+ when 'text/html'
616
+ load_html(remote_doc.document,
617
+ url: remote_doc.documentUrl,
618
+ extractAllScripts: extractAllScripts,
619
+ profile: profile,
620
+ **options) do |base|
621
+ remote_doc.documentUrl = base
622
+ end
623
+ else
624
+ validate_input(remote_doc.document, url: remote_doc.documentUrl) if validate
625
+ MultiJson.load(remote_doc.document, **options)
539
626
  end
540
- raise JSON::LD::JsonLdError::MultipleContextLinkHeaders,
541
- "expected at most 1 Link header with rel=jsonld:context, got #{links.length}" if links.length > 1
542
- Array(links.first).first
543
627
  end
544
628
 
545
- doc_uri = remote_doc.base_uri rescue url
546
- doc = RemoteDocument.new(doc_uri, remote_doc.read, contextUrl)
547
- block_given? ? yield(doc) : doc
629
+ if remote_doc.contentType && validate
630
+ raise IOError, "url: #{url}, contentType: #{remote_doc.contentType}" unless
631
+ remote_doc.contentType.match?(/application\/(.+\+)?json|text\/html/)
632
+ end
633
+ block_given? ? yield(remote_doc) : remote_doc
548
634
  end
549
- rescue IOError => e
635
+ rescue IOError, MultiJson::ParseError => e
550
636
  raise JSON::LD::JsonLdError::LoadingDocumentFailed, e.message
551
637
  end
552
638
 
639
+ ##
640
+ # Default document loader.
641
+ # @param [RDF::URI, String] url
642
+ # @param [Boolean] extractAllScripts
643
+ # If set to `true`, when extracting JSON-LD script elements from HTML, unless a specific fragment identifier is targeted, extracts all encountered JSON-LD script elements using an array form, if necessary.
644
+ # @param [String] profile
645
+ # When the resulting `contentType` is `text/html`, this option determines the profile to use for selecting a JSON-LD script elements.
646
+ # @param [String] requestProfile
647
+ # One or more IRIs to use in the request as a profile parameter.
648
+ # @param [Hash<Symbol => Object>] options
649
+ # @yield remote_document
650
+ # @yieldparam [RemoteDocument, RDF::Util::File::RemoteDocument] remote_document
651
+ # @raise [IOError]
652
+ def self.documentLoader(url, extractAllScripts: false, profile: nil, requestProfile: nil, **options, &block)
653
+ case url
654
+ when IO, StringIO
655
+ base_uri = options[:base]
656
+ base_uri ||= url.base_uri if url.respond_to?(:base_uri)
657
+ content_type = options[:content_type]
658
+ content_type ||= url.content_type if url.respond_to?(:content_type)
659
+ context_url = if url.respond_to?(:links) && url.links
660
+ (content_type == 'appliaction/json' || content_type.match?(%r(application/(^ld)+json)))
661
+ link = url.links.find_link(LINK_REL_CONTEXT)
662
+ link.href if link
663
+ end
664
+
665
+ block.call(RemoteDocument.new(url.read,
666
+ documentUrl: base_uri,
667
+ contentType: content_type,
668
+ contextUrl: context_url))
669
+ else
670
+ RDF::Util::File.open_file(url, **options, &block)
671
+ end
672
+ end
673
+
553
674
  # Add class method aliases for backwards compatibility
554
675
  class << self
555
676
  alias :toRDF :toRdf
556
677
  alias :fromRDF :fromRdf
557
678
  end
558
679
 
680
+ ##
681
+ # Load one or more script tags from an HTML source.
682
+ # Unescapes and uncomments input, returns the internal representation
683
+ # Yields document base
684
+ # @param [String] input
685
+ # @param [String] url Original URL
686
+ # @param [:nokogiri, :rexml] library (nil)
687
+ # @param [Boolean] extractAllScripts (false)
688
+ # @param [Boolean] profile (nil) Optional priortized profile when loading a single script by type.
689
+ # @param [Hash{Symbol => Object}] options
690
+ def self.load_html(input, url:,
691
+ library: nil,
692
+ extractAllScripts: false,
693
+ profile: nil,
694
+ **options)
695
+
696
+ if input.is_a?(String)
697
+ library ||= begin
698
+ require 'nokogiri'
699
+ :nokogiri
700
+ rescue LoadError
701
+ :rexml
702
+ end
703
+ require "json/ld/html/#{library}"
704
+
705
+ # Parse HTML using the appropriate library
706
+ @implementation = case library
707
+ when :nokogiri then Nokogiri
708
+ when :rexml then REXML
709
+ end
710
+ self.extend(@implementation)
711
+
712
+ input = begin
713
+ initialize_html(input, **options)
714
+ rescue
715
+ raise JSON::LD::JsonLdError::LoadingDocumentFailed, "Malformed HTML document: #{$!.message}"
716
+ end
717
+
718
+ # Potentially update options[:base]
719
+ if html_base = input.at_xpath("/html/head/base/@href")
720
+ base = RDF::URI(url) if url
721
+ html_base = RDF::URI(html_base)
722
+ html_base = base.join(html_base) if base
723
+ yield html_base
724
+ end
725
+ end
726
+
727
+ url = RDF::URI.parse(url)
728
+ if url.fragment
729
+ id = CGI.unescape(url.fragment)
730
+ # Find script with an ID based on that fragment.
731
+ element = input.at_xpath("//script[@id='#{id}']")
732
+ raise JSON::LD::JsonLdError::InvalidScriptElement, "No script tag found with id=#{id}" unless element
733
+ raise JSON::LD::JsonLdError::InvalidScriptElement, "Script tag has type=#{element.attributes['type']}" unless element.attributes['type'].to_s.start_with?('application/ld+json')
734
+ content = element.inner_html
735
+ validate_input(content, url: url) if options[:validate]
736
+ MultiJson.load(content, **options)
737
+ elsif extractAllScripts
738
+ res = []
739
+ elements = if profile
740
+ es = input.xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]")
741
+ # If no profile script, just take a single script without profile
742
+ es = [input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")] if es.empty?
743
+ es
744
+ else
745
+ input.xpath("//script[starts-with(@type, 'application/ld+json')]")
746
+ end
747
+ elements.each do |element|
748
+ content = element.inner_html
749
+ validate_input(content, url: url) if options[:validate]
750
+ r = MultiJson.load(content, **options)
751
+ if r.is_a?(Hash)
752
+ res << r
753
+ elsif r.is_a?(Array)
754
+ res = res.concat(r)
755
+ end
756
+ end
757
+ res
758
+ else
759
+ # Find the first script with type application/ld+json.
760
+ element = input.at_xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]") if profile
761
+ element ||= input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")
762
+ content = element ? element.inner_html : "[]"
763
+ validate_input(content, url: url) if options[:validate]
764
+ MultiJson.load(content, **options)
765
+ end
766
+ rescue JSON::LD::JsonLdError::LoadingDocumentFailed, MultiJson::ParseError => e
767
+ raise JSON::LD::JsonLdError::InvalidScriptElement, e.message
768
+ end
769
+
770
+ ##
771
+ # Validate JSON using JsonLint, if loaded
559
772
  private
560
- def validate_input(input)
773
+ def self.validate_input(input, url:)
561
774
  return unless defined?(JsonLint)
562
775
  jsonlint = JsonLint::Linter.new
563
776
  input = StringIO.new(input) unless input.respond_to?(:read)
564
777
  unless jsonlint.check_stream(input)
565
- raise JsonLdError::LoadingDocumentFailed, jsonlint.errors[''].join("\n")
778
+ raise JsonLdError::LoadingDocumentFailed, "url: #{url}\n" + jsonlint.errors[''].join("\n")
566
779
  end
567
780
  input.rewind
568
781
  end
@@ -570,26 +783,42 @@ module JSON::LD
570
783
  ##
571
784
  # A {RemoteDocument} is returned from a {documentLoader}.
572
785
  class RemoteDocument
573
- # @return [String] URL of the loaded document, after redirects
574
- attr_reader :documentUrl
786
+ # The final URL of the loaded document. This is important to handle HTTP redirects properly.
787
+ # @return [String]
788
+ attr_accessor :documentUrl
575
789
 
576
- # @return [String, Array<Hash>, Hash]
577
- # The retrieved document, either as raw text or parsed JSON
578
- attr_reader :document
790
+ # The Content-Type of the loaded document, exclusive of any optional parameters.
791
+ # @return [String]
792
+ attr_reader :contentType
579
793
 
580
794
  # @return [String]
581
795
  # The URL of a remote context as specified by an HTTP Link header with rel=`http://www.w3.org/ns/json-ld#context`
582
796
  attr_accessor :contextUrl
583
797
 
584
- # @param [String] url URL of the loaded document, after redirects
585
- # @param [String, Array<Hash>, Hash] document
586
- # The retrieved document, either as raw text or parsed JSON
587
- # @param [String] context_url (nil)
798
+ # The parsed retrieved document.
799
+ # @return [Array<Hash>, Hash]
800
+ attr_accessor :document
801
+
802
+ # The value of any profile parameter retrieved as part of the original contentType.
803
+ # @return [String]
804
+ attr_accessor :profile
805
+
806
+ # @param [RDF::Util::File::RemoteDocument] document
807
+ # @param [String] documentUrl
808
+ # The final URL of the loaded document. This is important to handle HTTP redirects properly.
809
+ # @param [String] contentType
810
+ # The Content-Type of the loaded document, exclusive of any optional parameters.
811
+ # @param [String] contextUrl
588
812
  # The URL of a remote context as specified by an HTTP Link header with rel=`http://www.w3.org/ns/json-ld#context`
589
- def initialize(url, document, context_url = nil)
590
- @documentUrl = url
813
+ # @param [String] profile
814
+ # The value of any profile parameter retrieved as part of the original contentType.
815
+ # @option options [Hash{Symbol => Object}] options
816
+ def initialize(document, documentUrl: nil, contentType: nil, contextUrl: nil, profile: nil, **options)
591
817
  @document = document
592
- @contextUrl = context_url
818
+ @documentUrl = documentUrl || options[:base_uri]
819
+ @contentType = contentType || options[:content_type]
820
+ @contextUrl = contextUrl
821
+ @profile = profile
593
822
  end
594
823
  end
595
824
  end