json-ld 3.0.2 → 3.1.0
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 +4 -4
- data/AUTHORS +1 -1
- data/README.md +90 -53
- data/UNLICENSE +1 -1
- data/VERSION +1 -1
- data/bin/jsonld +4 -4
- data/lib/json/ld.rb +27 -10
- data/lib/json/ld/api.rb +325 -96
- data/lib/json/ld/compact.rb +75 -27
- data/lib/json/ld/conneg.rb +188 -0
- data/lib/json/ld/context.rb +677 -292
- data/lib/json/ld/expand.rb +240 -75
- data/lib/json/ld/flatten.rb +5 -3
- data/lib/json/ld/format.rb +19 -19
- data/lib/json/ld/frame.rb +135 -85
- data/lib/json/ld/from_rdf.rb +44 -17
- data/lib/json/ld/html/nokogiri.rb +151 -0
- data/lib/json/ld/html/rexml.rb +186 -0
- data/lib/json/ld/reader.rb +25 -5
- data/lib/json/ld/resource.rb +2 -2
- data/lib/json/ld/streaming_writer.rb +3 -1
- data/lib/json/ld/to_rdf.rb +47 -17
- data/lib/json/ld/utils.rb +4 -2
- data/lib/json/ld/writer.rb +75 -14
- data/spec/api_spec.rb +13 -34
- data/spec/compact_spec.rb +968 -9
- data/spec/conneg_spec.rb +373 -0
- data/spec/context_spec.rb +447 -53
- data/spec/expand_spec.rb +1872 -416
- data/spec/flatten_spec.rb +434 -47
- data/spec/frame_spec.rb +979 -344
- data/spec/from_rdf_spec.rb +305 -5
- data/spec/spec_helper.rb +177 -0
- data/spec/streaming_writer_spec.rb +4 -4
- data/spec/suite_compact_spec.rb +2 -2
- data/spec/suite_expand_spec.rb +14 -2
- data/spec/suite_flatten_spec.rb +10 -2
- data/spec/suite_frame_spec.rb +3 -2
- data/spec/suite_from_rdf_spec.rb +2 -2
- data/spec/suite_helper.rb +55 -20
- data/spec/suite_html_spec.rb +22 -0
- data/spec/suite_http_spec.rb +35 -0
- data/spec/suite_remote_doc_spec.rb +2 -2
- data/spec/suite_to_rdf_spec.rb +14 -3
- data/spec/support/extensions.rb +5 -1
- data/spec/test-files/test-4-input.json +3 -3
- data/spec/test-files/test-5-input.json +2 -2
- data/spec/test-files/test-8-framed.json +14 -18
- data/spec/to_rdf_spec.rb +606 -16
- data/spec/writer_spec.rb +5 -5
- metadata +144 -88
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9fcb46a6c6bf33bfed92adcc8a751598cec5d6511e0450d38c9920bfdb696ce
|
4
|
+
data.tar.gz: 6fec905be8d5e0335149da757fef899d6e3fa948e852f74e9c88d0bff45801ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a7d07de17bc462f2f38fc24ae7c2a04a2c1ffd24d0a431626771259d59ffd6fe4b33b4ea8501add71d710e3c4501897454aada60ff6054fa9a12ef69cf488f3
|
7
|
+
data.tar.gz: 48da4933e6660ccd7cffe9cdcb08b576d883f1c744a889867b5e8619f9c3908fad00af02a739bc9af5c1abd7495d3cb9517d914ee12f7ac4f48acf94b5b9b637
|
data/AUTHORS
CHANGED
@@ -1 +1 @@
|
|
1
|
-
* Gregg Kellogg <gregg@
|
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
|
-
[](
|
6
|
-
[](
|
5
|
+
[](https://badge.fury.io/rb/json-ld)
|
6
|
+
[](https://travis-ci.org/ruby-rdf/json-ld)
|
7
7
|
[](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"=>"
|
52
|
-
"http://xmlns.com/foaf/0.1/avatar": [{"@value": "
|
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": "
|
60
|
-
"http://xmlns.com/foaf/0.1/avatar": [{"@id": "
|
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": "
|
79
|
-
"homepage": "
|
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
|
-
"": "
|
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
|
-
<
|
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
|
-
"": "
|
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": "
|
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
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
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
|
-
|
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
|
-
|
437
|
-
|
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
|
-
|
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](
|
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](
|
472
|
-
* [RDF.rb](
|
473
|
-
* [JSON](https://rubygems.org/gems/json) (>= 2.
|
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](
|
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
|
-
* <
|
521
|
+
* <https://lists.w3.org/Archives/Public/public-rdf-ruby/>
|
488
522
|
|
489
523
|
## Author
|
490
|
-
* [Gregg Kellogg](
|
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 <
|
510
|
-
|
511
|
-
[Ruby]:
|
512
|
-
[RDF]:
|
513
|
-
[YARD]:
|
514
|
-
[YARD-GS]:
|
515
|
-
[PDD]:
|
516
|
-
[RDF.rb]:
|
517
|
-
[
|
518
|
-
[
|
519
|
-
[JSON-LD
|
520
|
-
[JSON-LD
|
521
|
-
[
|
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 <
|
24
|
+
For more information, please refer to <https://unlicense.org/>
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.0
|
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] =
|
174
|
+
options[:embed] = '@never'
|
175
175
|
when 'false'
|
176
|
-
options[:embed] =
|
176
|
+
options[:embed] = '@first'
|
177
177
|
else
|
178
|
-
STDERR.puts "--embed option takes one of
|
178
|
+
STDERR.puts "--embed option takes one of @always, @never, @link, @first, or @last"
|
179
179
|
exit(1)
|
180
180
|
end
|
181
181
|
end
|
data/lib/json/ld.rb
CHANGED
@@ -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
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
143
|
-
class
|
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
|
data/lib/json/ld/api.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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 ||=
|
133
|
-
@context = Context.parse(context || {},
|
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
|
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
|
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,
|
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
|
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 = {
|
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 ['@
|
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
|
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: '@
|
341
|
+
embed: '@once',
|
342
342
|
explicit: false,
|
343
|
-
requireAll:
|
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
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
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,
|
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,
|
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] =
|
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
|
-
|
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
|
-
#
|
412
|
-
|
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
|
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,
|
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
|
-
#
|
532
|
+
# Uses built-in or provided documentLoader to retrieve a parsed document.
|
533
|
+
#
|
511
534
|
# @param [RDF::URI, String] url
|
512
|
-
# @param [
|
513
|
-
#
|
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.
|
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
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
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
|
-
#
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
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
|
-
|
546
|
-
|
547
|
-
|
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
|
-
#
|
574
|
-
|
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
|
-
#
|
577
|
-
#
|
578
|
-
attr_reader :
|
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
|
-
#
|
585
|
-
# @
|
586
|
-
|
587
|
-
|
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
|
-
|
590
|
-
|
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
|
-
@
|
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
|