json-ld 3.1.0 → 3.1.5
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/README.md +66 -46
- data/VERSION +1 -1
- data/bin/jsonld +27 -30
- data/lib/json/ld.rb +12 -8
- data/lib/json/ld/api.rb +51 -43
- data/lib/json/ld/compact.rb +82 -68
- data/lib/json/ld/conneg.rb +1 -1
- data/lib/json/ld/context.rb +650 -542
- data/lib/json/ld/expand.rb +154 -87
- data/lib/json/ld/flatten.rb +1 -1
- data/lib/json/ld/format.rb +10 -6
- data/lib/json/ld/frame.rb +1 -2
- data/lib/json/ld/from_rdf.rb +7 -8
- data/lib/json/ld/html/nokogiri.rb +2 -1
- data/lib/json/ld/html/rexml.rb +2 -1
- data/lib/json/ld/reader.rb +20 -11
- data/lib/json/ld/streaming_reader.rb +578 -0
- data/lib/json/ld/to_rdf.rb +9 -3
- data/lib/json/ld/writer.rb +12 -5
- data/spec/compact_spec.rb +1 -0
- data/spec/context_spec.rb +63 -116
- data/spec/expand_spec.rb +29 -9
- data/spec/frame_spec.rb +44 -0
- data/spec/matchers.rb +1 -1
- data/spec/reader_spec.rb +33 -34
- data/spec/streaming_reader_spec.rb +237 -0
- data/spec/suite_expand_spec.rb +4 -2
- data/spec/suite_frame_spec.rb +0 -1
- data/spec/suite_helper.rb +23 -8
- data/spec/suite_to_rdf_spec.rb +1 -1
- data/spec/to_rdf_spec.rb +3 -3
- metadata +11 -8
data/lib/json/ld.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
# frozen_string_literal: true
|
3
3
|
$:.unshift(File.expand_path("../ld", __FILE__))
|
4
|
-
require 'rdf' # @see
|
4
|
+
require 'rdf' # @see https://rubygems.org/gems/rdf
|
5
5
|
require 'multi_json'
|
6
6
|
require 'set'
|
7
7
|
|
@@ -19,7 +19,7 @@ module JSON
|
|
19
19
|
# end
|
20
20
|
# end
|
21
21
|
#
|
22
|
-
# @see
|
22
|
+
# @see https://rubygems.org/gems/rdf
|
23
23
|
# @see http://www.w3.org/TR/REC-rdf-syntax/
|
24
24
|
#
|
25
25
|
# @author [Gregg Kellogg](http://greggkellogg.net/)
|
@@ -34,6 +34,8 @@ module JSON
|
|
34
34
|
autoload :Normalize, 'json/ld/normalize'
|
35
35
|
autoload :Reader, 'json/ld/reader'
|
36
36
|
autoload :Resource, 'json/ld/resource'
|
37
|
+
autoload :StreamingReader, 'json/ld/streaming_reader'
|
38
|
+
autoload :StreamingWriter, 'json/ld/streaming_writer'
|
37
39
|
autoload :VERSION, 'json/ld/version'
|
38
40
|
autoload :Writer, 'json/ld/writer'
|
39
41
|
|
@@ -52,13 +54,13 @@ module JSON
|
|
52
54
|
@direction
|
53
55
|
@embed
|
54
56
|
@explicit
|
55
|
-
@json
|
56
|
-
@id
|
57
|
-
@included
|
58
|
-
@index
|
59
57
|
@first
|
60
58
|
@graph
|
59
|
+
@id
|
61
60
|
@import
|
61
|
+
@included
|
62
|
+
@index
|
63
|
+
@json
|
62
64
|
@language
|
63
65
|
@list
|
64
66
|
@nest
|
@@ -66,6 +68,7 @@ module JSON
|
|
66
68
|
@omitDefault
|
67
69
|
@propagate
|
68
70
|
@protected
|
71
|
+
@preserve
|
69
72
|
@requireAll
|
70
73
|
@reverse
|
71
74
|
@set
|
@@ -115,7 +118,7 @@ module JSON
|
|
115
118
|
class CyclicIRIMapping < JsonLdError; @code = "cyclic IRI mapping"; end
|
116
119
|
class InvalidBaseIRI < JsonLdError; @code = "invalid base IRI"; end
|
117
120
|
class InvalidContainerMapping < JsonLdError; @code = "invalid container mapping"; end
|
118
|
-
class
|
121
|
+
class InvalidContextEntry < JsonLdError; @code = "invalid context entry"; end
|
119
122
|
class InvalidContextNullification < JsonLdError; @code = "invalid context nullification"; end
|
120
123
|
class InvalidDefaultLanguage < JsonLdError; @code = "invalid default language"; end
|
121
124
|
class InvalidIdValue < JsonLdError; @code = "invalid @id value"; end
|
@@ -142,6 +145,7 @@ module JSON
|
|
142
145
|
class InvalidScopedContext < JsonLdError; @code = "invalid scoped context"; end
|
143
146
|
class InvalidScriptElement < JsonLdError; @code = "invalid script element"; end
|
144
147
|
class InvalidSetOrListObject < JsonLdError; @code = "invalid set or list object"; end
|
148
|
+
class InvalidStreamingKeyOrder < JsonLdError; @code = 'invalid streaming key order' end
|
145
149
|
class InvalidTermDefinition < JsonLdError; @code = "invalid term definition"; end
|
146
150
|
class InvalidBaseDirection < JsonLdError; @code = "invalid base direction"; end
|
147
151
|
class InvalidTypedValue < JsonLdError; @code = "invalid typed value"; end
|
@@ -154,7 +158,7 @@ module JSON
|
|
154
158
|
class KeywordRedefinition < JsonLdError; @code = "keyword redefinition"; end
|
155
159
|
class LoadingDocumentFailed < JsonLdError; @code = "loading document failed"; end
|
156
160
|
class LoadingRemoteContextFailed < JsonLdError; @code = "loading remote context failed"; end
|
157
|
-
class ContextOverflow < JsonLdError; @code = "
|
161
|
+
class ContextOverflow < JsonLdError; @code = "context overflow"; end
|
158
162
|
class MissingIncludedReferent < JsonLdError; @code = "missing @included referent"; end
|
159
163
|
class MultipleContextLinkHeaders < JsonLdError; @code = "multiple context link headers"; end
|
160
164
|
class ProtectedTermRedefinition < JsonLdError; @code = "protected term redefinition"; end
|
data/lib/json/ld/api.rb
CHANGED
@@ -35,7 +35,7 @@ module JSON::LD
|
|
35
35
|
|
36
36
|
# Options used for open_file
|
37
37
|
OPEN_OPTS = {
|
38
|
-
headers: {"Accept" => "application/ld+json, text/html;q=0.8, application/json;q=0.5"}
|
38
|
+
headers: {"Accept" => "application/ld+json, text/html;q=0.8, application/xhtml+xml;q=0.8, application/json;q=0.5"}
|
39
39
|
}
|
40
40
|
|
41
41
|
# The following constants are used to reduce object allocations
|
@@ -66,7 +66,7 @@ module JSON::LD
|
|
66
66
|
# @param [String, #read, Hash, Array, JSON::LD::Context] context
|
67
67
|
# An external context to use additionally to the context embedded in input when expanding the input.
|
68
68
|
# @param [Hash{Symbol => Object}] options
|
69
|
-
# @option options [String, #to_s] :base
|
69
|
+
# @option options [RDF::URI, String, #to_s] :base
|
70
70
|
# The Base IRI to use when expanding the document. This overrides the value of `input` if it is a _IRI_. If not specified and `input` is not an _IRI_, the base IRI defaults to the current document IRI if in a browser context, or the empty string if there is no document context. If not specified, and a base IRI is found from `input`, options[:base] will be modified with this value.
|
71
71
|
# @option options [Boolean] :compactArrays (true)
|
72
72
|
# If set to `true`, the JSON-LD processor replaces arrays with just one element with that element during compaction. If set to `false`, all arrays will remain arrays even if they have just one element.
|
@@ -108,6 +108,7 @@ module JSON::LD
|
|
108
108
|
}.merge(options)
|
109
109
|
@namer = unique_bnodes ? BlankNodeUniqer.new : (rename_bnodes ? BlankNodeNamer.new("b") : BlankNodeMapper.new)
|
110
110
|
|
111
|
+
@options[:base] = RDF::URI(@options[:base]) if @options[:base] && !@options[:base].is_a?(RDF::URI)
|
111
112
|
# For context via Link header
|
112
113
|
_, context_ref = nil, nil
|
113
114
|
|
@@ -117,7 +118,7 @@ module JSON::LD
|
|
117
118
|
remote_doc = self.class.loadRemoteDocument(input, **@options)
|
118
119
|
|
119
120
|
context_ref = remote_doc.contextUrl
|
120
|
-
@options[:base] = remote_doc.documentUrl if remote_doc.documentUrl && !@options[:no_default_base]
|
121
|
+
@options[:base] = RDF::URI(remote_doc.documentUrl) if remote_doc.documentUrl && !@options[:no_default_base]
|
121
122
|
|
122
123
|
case remote_doc.document
|
123
124
|
when String
|
@@ -130,7 +131,7 @@ module JSON::LD
|
|
130
131
|
|
131
132
|
# If not provided, first use context from document, or from a Link header
|
132
133
|
context ||= context_ref || {}
|
133
|
-
@context = Context.parse(context
|
134
|
+
@context = Context.parse(context, **@options)
|
134
135
|
|
135
136
|
if block_given?
|
136
137
|
case block.arity
|
@@ -163,10 +164,9 @@ module JSON::LD
|
|
163
164
|
# If a block is given, the result of evaluating the block is returned, otherwise, the expanded JSON-LD document
|
164
165
|
# @see https://www.w3.org/TR/json-ld11-api/#expansion-algorithm
|
165
166
|
def self.expand(input, framing: false, **options, &block)
|
166
|
-
result
|
167
|
+
result = doc_base = nil
|
167
168
|
API.new(input, options[:expandContext], **options) do
|
168
169
|
result = self.expand(self.value, nil, self.context,
|
169
|
-
ordered: @options[:ordered],
|
170
170
|
framing: framing)
|
171
171
|
doc_base = @options[:base]
|
172
172
|
end
|
@@ -218,21 +218,21 @@ module JSON::LD
|
|
218
218
|
# 1) Perform the Expansion Algorithm on the JSON-LD input.
|
219
219
|
# This removes any existing context to allow the given context to be cleanly applied.
|
220
220
|
expanded_input = expanded ? input : API.expand(input, ordered: false, **options) do |res, base_iri|
|
221
|
-
options[:base] ||= base_iri if options[:compactToRelative]
|
221
|
+
options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
|
222
222
|
res
|
223
223
|
end
|
224
224
|
|
225
225
|
API.new(expanded_input, context, no_default_base: true, **options) do
|
226
226
|
log_debug(".compact") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
|
227
|
-
result = compact(value
|
227
|
+
result = compact(value)
|
228
228
|
|
229
229
|
# xxx) Add the given context to the output
|
230
|
-
ctx = self.context.serialize
|
230
|
+
ctx = self.context.serialize(provided_context: context)
|
231
231
|
if result.is_a?(Array)
|
232
|
-
kwgraph = self.context.compact_iri('@graph', vocab: true
|
232
|
+
kwgraph = self.context.compact_iri('@graph', vocab: true)
|
233
233
|
result = result.empty? ? {} : {kwgraph => result}
|
234
234
|
end
|
235
|
-
result = ctx.merge(result) unless ctx.empty?
|
235
|
+
result = ctx.merge(result) unless ctx.fetch('@context', {}).empty?
|
236
236
|
end
|
237
237
|
block_given? ? yield(result) : result
|
238
238
|
end
|
@@ -265,7 +265,7 @@ module JSON::LD
|
|
265
265
|
|
266
266
|
# Expand input to simplify processing
|
267
267
|
expanded_input = expanded ? input : API.expand(input, **options) do |result, base_iri|
|
268
|
-
options[:base] ||= base_iri if options[:compactToRelative]
|
268
|
+
options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
|
269
269
|
result
|
270
270
|
end
|
271
271
|
|
@@ -294,9 +294,11 @@ module JSON::LD
|
|
294
294
|
|
295
295
|
if context && !flattened.empty?
|
296
296
|
# Otherwise, return the result of compacting flattened according the Compaction algorithm passing context ensuring that the compaction result uses the @graph keyword (or its alias) at the top-level, even if the context is empty or if there is only one element to put in the @graph array. This ensures that the returned document has a deterministic structure.
|
297
|
-
compacted = as_array(compact(flattened
|
298
|
-
kwgraph = self.context.compact_iri('@graph'
|
299
|
-
flattened = self.context.
|
297
|
+
compacted = as_array(compact(flattened))
|
298
|
+
kwgraph = self.context.compact_iri('@graph')
|
299
|
+
flattened = self.context.
|
300
|
+
serialize(provided_context: context).
|
301
|
+
merge(kwgraph => compacted)
|
300
302
|
end
|
301
303
|
end
|
302
304
|
|
@@ -313,11 +315,11 @@ module JSON::LD
|
|
313
315
|
# @param [String, #read, Hash, Array] frame
|
314
316
|
# The frame to use when re-arranging the data.
|
315
317
|
# @option options (see #initialize)
|
316
|
-
# @option options ['@always', '@
|
318
|
+
# @option options ['@always', '@link', '@once', '@never'] :embed ('@once')
|
317
319
|
# a flag specifying that objects should be directly embedded in the output, instead of being referred to by their IRI.
|
318
320
|
# @option options [Boolean] :explicit (false)
|
319
321
|
# a flag specifying that for properties to be included in the output, they must be explicitly declared in the framing context.
|
320
|
-
# @option options [Boolean] :requireAll (
|
322
|
+
# @option options [Boolean] :requireAll (false)
|
321
323
|
# A flag specifying that all properties present in the input frame must either have a default value or be present in the JSON-LD input for the frame to match.
|
322
324
|
# @option options [Boolean] :omitDefault (false)
|
323
325
|
# a flag specifying that properties that are missing from the JSON-LD input should be omitted from the output.
|
@@ -335,7 +337,7 @@ module JSON::LD
|
|
335
337
|
def self.frame(input, frame, expanded: false, **options)
|
336
338
|
result = nil
|
337
339
|
options = {
|
338
|
-
base: (input if input.is_a?(String)),
|
340
|
+
base: (RDF::URI(input) if input.is_a?(String)),
|
339
341
|
compactArrays: true,
|
340
342
|
compactToRelative: true,
|
341
343
|
embed: '@once',
|
@@ -369,7 +371,7 @@ module JSON::LD
|
|
369
371
|
|
370
372
|
# Expand input to simplify processing
|
371
373
|
expanded_input = expanded ? input : API.expand(input, ordered: false, **options) do |res, base_iri|
|
372
|
-
options[:base] ||= base_iri if options[:compactToRelative]
|
374
|
+
options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
|
373
375
|
res
|
374
376
|
end
|
375
377
|
|
@@ -394,7 +396,7 @@ module JSON::LD
|
|
394
396
|
# Get framing nodes from expanded input, replacing Blank Node identifiers as necessary
|
395
397
|
create_node_map(value, framing_state[:graphMap], active_graph: '@default')
|
396
398
|
|
397
|
-
frame_keys = frame.keys.map {|k| context.expand_iri(k, vocab: true
|
399
|
+
frame_keys = frame.keys.map {|k| context.expand_iri(k, vocab: true)}
|
398
400
|
if frame_keys.include?('@graph')
|
399
401
|
# If frame contains @graph, it matches the default graph.
|
400
402
|
framing_state[:graph] = '@default'
|
@@ -426,7 +428,7 @@ module JSON::LD
|
|
426
428
|
log_debug(".frame") {"expanded result: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
|
427
429
|
|
428
430
|
# Compact result
|
429
|
-
compacted = compact(result
|
431
|
+
compacted = compact(result)
|
430
432
|
|
431
433
|
# @replace `@null` with nil, compacting arrays
|
432
434
|
compacted = cleanup_null(compacted)
|
@@ -434,11 +436,14 @@ module JSON::LD
|
|
434
436
|
|
435
437
|
# Add the given context to the output
|
436
438
|
result = if !compacted.is_a?(Array)
|
437
|
-
|
439
|
+
compacted
|
438
440
|
else
|
439
|
-
kwgraph = context.compact_iri('@graph'
|
440
|
-
|
441
|
+
kwgraph = context.compact_iri('@graph')
|
442
|
+
{kwgraph => compacted}
|
441
443
|
end
|
444
|
+
# Only add context if one was provided
|
445
|
+
result = context.serialize(provided_context: frame).merge(result) if frame['@context']
|
446
|
+
|
442
447
|
log_debug(".frame") {"after compact: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
|
443
448
|
result
|
444
449
|
end
|
@@ -521,8 +526,7 @@ module JSON::LD
|
|
521
526
|
API.new(nil, nil, **options) do
|
522
527
|
result = from_statements(input,
|
523
528
|
useRdfType: useRdfType,
|
524
|
-
useNativeTypes: useNativeTypes
|
525
|
-
ordered: @options[:ordered])
|
529
|
+
useNativeTypes: useNativeTypes)
|
526
530
|
end
|
527
531
|
|
528
532
|
block_given? ? yield(result) : result
|
@@ -532,16 +536,18 @@ module JSON::LD
|
|
532
536
|
# Uses built-in or provided documentLoader to retrieve a parsed document.
|
533
537
|
#
|
534
538
|
# @param [RDF::URI, String] url
|
539
|
+
# @param [String, RDF::URI] base
|
540
|
+
# Location to use as documentUrl instead of `url`.
|
541
|
+
# @option options [Proc] :documentLoader
|
542
|
+
# The callback of the loader to be used to retrieve remote documents and contexts.
|
535
543
|
# @param [Boolean] extractAllScripts
|
536
544
|
# 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
545
|
# @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.
|
546
|
+
# When the resulting `contentType` is `text/html` or `application/xhtml+xml`, this option determines the profile to use for selecting a JSON-LD script elements.
|
539
547
|
# @param [String] requestProfile
|
540
548
|
# One or more IRIs to use in the request as a profile parameter.
|
541
|
-
# @param [Boolean] validate
|
549
|
+
# @param [Boolean] validate (false)
|
542
550
|
# Allow only appropriate content types
|
543
|
-
# @param [String, RDF::URI] base
|
544
|
-
# Location to use as documentUrl instead of `url`.
|
545
551
|
# @param [Hash<Symbol => Object>] options
|
546
552
|
# @yield remote_document
|
547
553
|
# @yieldparam [RemoteDocumentRemoteDocument, RDF::Util::File::RemoteDocument] remote_document
|
@@ -550,13 +556,14 @@ module JSON::LD
|
|
550
556
|
# If a block is given, the result of evaluating the block is returned, otherwise, the retrieved remote document and context information unless block given
|
551
557
|
# @raise [JsonLdError]
|
552
558
|
def self.loadRemoteDocument(url,
|
559
|
+
base: nil,
|
560
|
+
documentLoader: nil,
|
553
561
|
extractAllScripts: false,
|
554
562
|
profile: nil,
|
555
563
|
requestProfile: nil,
|
556
564
|
validate: false,
|
557
|
-
base: nil,
|
558
565
|
**options)
|
559
|
-
documentLoader
|
566
|
+
documentLoader ||= self.method(:documentLoader)
|
560
567
|
options = OPEN_OPTS.merge(options)
|
561
568
|
if requestProfile
|
562
569
|
# Add any request profile
|
@@ -612,7 +619,7 @@ module JSON::LD
|
|
612
619
|
# Parse any HTML
|
613
620
|
if remote_doc.document.is_a?(String)
|
614
621
|
remote_doc.document = case remote_doc.contentType
|
615
|
-
when 'text/html'
|
622
|
+
when 'text/html', 'application/xhtml+xml'
|
616
623
|
load_html(remote_doc.document,
|
617
624
|
url: remote_doc.documentUrl,
|
618
625
|
extractAllScripts: extractAllScripts,
|
@@ -628,7 +635,7 @@ module JSON::LD
|
|
628
635
|
|
629
636
|
if remote_doc.contentType && validate
|
630
637
|
raise IOError, "url: #{url}, contentType: #{remote_doc.contentType}" unless
|
631
|
-
remote_doc.contentType.match?(/application\/(.+\+)?json|text\/html/)
|
638
|
+
remote_doc.contentType.match?(/application\/(.+\+)?json|text\/html|application\/xhtml\+xml/)
|
632
639
|
end
|
633
640
|
block_given? ? yield(remote_doc) : remote_doc
|
634
641
|
end
|
@@ -642,7 +649,7 @@ module JSON::LD
|
|
642
649
|
# @param [Boolean] extractAllScripts
|
643
650
|
# 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
651
|
# @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.
|
652
|
+
# When the resulting `contentType` is `text/html` or `application/xhtml+xml`, this option determines the profile to use for selecting a JSON-LD script elements.
|
646
653
|
# @param [String] requestProfile
|
647
654
|
# One or more IRIs to use in the request as a profile parameter.
|
648
655
|
# @param [Hash<Symbol => Object>] options
|
@@ -703,14 +710,14 @@ module JSON::LD
|
|
703
710
|
require "json/ld/html/#{library}"
|
704
711
|
|
705
712
|
# Parse HTML using the appropriate library
|
706
|
-
|
713
|
+
implementation = case library
|
707
714
|
when :nokogiri then Nokogiri
|
708
715
|
when :rexml then REXML
|
709
716
|
end
|
710
|
-
self.extend(
|
717
|
+
self.extend(implementation)
|
711
718
|
|
712
719
|
input = begin
|
713
|
-
|
720
|
+
self.send("initialize_html_#{library}".to_sym, input, **options)
|
714
721
|
rescue
|
715
722
|
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "Malformed HTML document: #{$!.message}"
|
716
723
|
end
|
@@ -729,8 +736,8 @@ module JSON::LD
|
|
729
736
|
id = CGI.unescape(url.fragment)
|
730
737
|
# Find script with an ID based on that fragment.
|
731
738
|
element = input.at_xpath("//script[@id='#{id}']")
|
732
|
-
raise JSON::LD::JsonLdError::
|
733
|
-
raise JSON::LD::JsonLdError::
|
739
|
+
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found with id=#{id}" unless element
|
740
|
+
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "Script tag has type=#{element.attributes['type']}" unless element.attributes['type'].to_s.start_with?('application/ld+json')
|
734
741
|
content = element.inner_html
|
735
742
|
validate_input(content, url: url) if options[:validate]
|
736
743
|
MultiJson.load(content, **options)
|
@@ -739,7 +746,7 @@ module JSON::LD
|
|
739
746
|
elements = if profile
|
740
747
|
es = input.xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]")
|
741
748
|
# 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?
|
749
|
+
es = [input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")].compact if es.empty?
|
743
750
|
es
|
744
751
|
else
|
745
752
|
input.xpath("//script[starts-with(@type, 'application/ld+json')]")
|
@@ -759,11 +766,12 @@ module JSON::LD
|
|
759
766
|
# Find the first script with type application/ld+json.
|
760
767
|
element = input.at_xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]") if profile
|
761
768
|
element ||= input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")
|
762
|
-
|
769
|
+
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found" unless element
|
770
|
+
content = element.inner_html
|
763
771
|
validate_input(content, url: url) if options[:validate]
|
764
772
|
MultiJson.load(content, **options)
|
765
773
|
end
|
766
|
-
rescue
|
774
|
+
rescue MultiJson::ParseError => e
|
767
775
|
raise JSON::LD::JsonLdError::InvalidScriptElement, e.message
|
768
776
|
end
|
769
777
|
|
data/lib/json/ld/compact.rb
CHANGED
@@ -5,28 +5,23 @@ module JSON::LD
|
|
5
5
|
include Utils
|
6
6
|
|
7
7
|
# The following constant is used to reduce object allocations in #compact below
|
8
|
-
CONTAINER_MAPPING_ID = %w(@id).freeze
|
9
|
-
CONTAINER_MAPPING_INDEX = %w(@index).freeze
|
10
|
-
CONTAINER_MAPPING_LANGUAGE = %w(@language).freeze
|
11
8
|
CONTAINER_MAPPING_LANGUAGE_INDEX_ID_TYPE = Set.new(%w(@language @index @id @type)).freeze
|
12
|
-
CONTAINER_MAPPING_LIST = %w(@list).freeze
|
13
|
-
CONTAINER_MAPPING_TYPE = %w(@type).freeze
|
14
9
|
EXPANDED_PROPERTY_DIRECTION_INDEX_LANGUAGE_VALUE = %w(@direction @index @language @value).freeze
|
15
10
|
|
16
11
|
##
|
17
12
|
# This algorithm compacts a JSON-LD document, such that the given context is applied. This must result in shortening any applicable IRIs to terms or compact IRIs, any applicable keywords to keyword aliases, and any applicable JSON-LD values expressed in expanded form to simple values such as strings or numbers.
|
18
13
|
#
|
19
14
|
# @param [Array, Hash] element
|
20
|
-
# @param [String]
|
21
|
-
# @param [Boolean] ordered (true)
|
15
|
+
# @param [String, RDF::URI] base (nil)
|
22
16
|
# Ensure output objects have keys ordered properly
|
17
|
+
# @param [String] property (nil)
|
18
|
+
# Extra validatation
|
23
19
|
# @return [Array, Hash]
|
24
|
-
def compact(element,
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
#end
|
20
|
+
def compact(element,
|
21
|
+
base: nil,
|
22
|
+
property: nil,
|
23
|
+
log_depth: nil)
|
24
|
+
log_debug("compact", depth: log_depth.to_i) {"element: #{element.inspect}, ec: #{context.inspect}"}
|
30
25
|
|
31
26
|
# If the term definition for active property itself contains a context, use that for compacting values.
|
32
27
|
input_context = self.context
|
@@ -34,17 +29,19 @@ module JSON::LD
|
|
34
29
|
case element
|
35
30
|
when Array
|
36
31
|
#log_debug("") {"Array #{element.inspect}"}
|
37
|
-
result = element.map
|
32
|
+
result = element.map do |item|
|
33
|
+
compact(item, base: base, property: property, log_depth: log_depth.to_i + 1)
|
34
|
+
end.compact
|
38
35
|
|
39
36
|
# If element has a single member and the active property has no
|
40
37
|
# @container mapping to @list or @set, the compacted value is that
|
41
38
|
# member; otherwise the compacted value is element
|
42
39
|
if result.length == 1 &&
|
43
40
|
!context.as_array?(property) && @options[:compactArrays]
|
44
|
-
|
41
|
+
log_debug("=> extract single element", depth: log_depth.to_i) {result.first.inspect}
|
45
42
|
result.first
|
46
43
|
else
|
47
|
-
|
44
|
+
log_debug("=> array result", depth: log_depth.to_i) {result.inspect}
|
48
45
|
result
|
49
46
|
end
|
50
47
|
when Hash
|
@@ -55,24 +52,31 @@ module JSON::LD
|
|
55
52
|
|
56
53
|
# Revert any previously type-scoped (non-preserved) context
|
57
54
|
if context.previous_context && !element.key?('@value') && element.keys != %w(@id)
|
55
|
+
log_debug("revert ec", depth: log_depth.to_i) {"previous context: #{context.previous_context.inspect}"}
|
58
56
|
self.context = context.previous_context
|
59
57
|
end
|
60
58
|
|
61
59
|
# Look up term definintions from property using the original type-scoped context, if it exists, but apply them to the now current previous context
|
62
60
|
td = input_context.term_definitions[property] if property
|
63
|
-
|
61
|
+
if td && !td.context.nil?
|
62
|
+
self.context = context.parse(td.context,
|
63
|
+
override_protected: true)
|
64
|
+
log_debug("prop-scoped", depth: log_depth.to_i) {"context: #{self.context.inspect}"}
|
65
|
+
end
|
64
66
|
|
65
67
|
if element.key?('@id') || element.key?('@value')
|
66
|
-
result = context.compact_value(property, element,
|
68
|
+
result = context.compact_value(property, element, base: @options[:base])
|
67
69
|
if !result.is_a?(Hash) || context.coerce(property) == '@json'
|
68
|
-
|
70
|
+
log_debug("", depth: log_depth.to_i) {"=> scalar result: #{result.inspect}"}
|
69
71
|
return result
|
70
72
|
end
|
71
73
|
end
|
72
74
|
|
73
75
|
# If expanded property is @list and we're contained within a list container, recursively compact this item to an array
|
74
|
-
if list?(element) && context.container(property)
|
75
|
-
return compact(element['@list'],
|
76
|
+
if list?(element) && context.container(property).include?('@list')
|
77
|
+
return compact(element['@list'], base: base,
|
78
|
+
property: property,
|
79
|
+
log_depth: log_depth.to_i + 1)
|
76
80
|
end
|
77
81
|
|
78
82
|
inside_reverse = property == '@reverse'
|
@@ -85,15 +89,18 @@ module JSON::LD
|
|
85
89
|
sort.
|
86
90
|
each do |term|
|
87
91
|
term_context = input_context.term_definitions[term].context if input_context.term_definitions[term]
|
88
|
-
self.context = context.parse(term_context, propagate: false)
|
92
|
+
self.context = context.parse(term_context, propagate: false) unless term_context.nil?
|
93
|
+
log_debug("type-scoped", depth: log_depth.to_i) {"context: #{self.context.inspect}"}
|
89
94
|
end
|
90
95
|
|
91
|
-
element.keys.opt_sort(ordered: ordered).each do |expanded_property|
|
96
|
+
element.keys.opt_sort(ordered: @options[:ordered]).each do |expanded_property|
|
92
97
|
expanded_value = element[expanded_property]
|
93
|
-
|
98
|
+
log_debug("", depth: log_depth.to_i) {"#{expanded_property}: #{expanded_value.inspect}"}
|
94
99
|
|
95
100
|
if expanded_property == '@id'
|
96
|
-
compacted_value = Array(expanded_value).map
|
101
|
+
compacted_value = Array(expanded_value).map do |expanded_id|
|
102
|
+
context.compact_iri(expanded_id, base: @options[:base])
|
103
|
+
end
|
97
104
|
|
98
105
|
kw_alias = context.compact_iri('@id', vocab: true)
|
99
106
|
as_array = compacted_value.length > 1
|
@@ -103,21 +110,24 @@ module JSON::LD
|
|
103
110
|
end
|
104
111
|
|
105
112
|
if expanded_property == '@type'
|
106
|
-
compacted_value = Array(expanded_value).map
|
113
|
+
compacted_value = Array(expanded_value).map do |expanded_type|
|
114
|
+
input_context.compact_iri(expanded_type, vocab: true)
|
115
|
+
end
|
107
116
|
|
108
117
|
kw_alias = context.compact_iri('@type', vocab: true)
|
109
118
|
as_array = compacted_value.length > 1 ||
|
110
119
|
(context.as_array?(kw_alias) &&
|
111
120
|
!value?(element) &&
|
112
121
|
context.processingMode('json-ld-1.1'))
|
113
|
-
|
114
|
-
result[kw_alias] = compacted_value
|
122
|
+
add_value(result, kw_alias, compacted_value, property_is_array: as_array)
|
115
123
|
next
|
116
124
|
end
|
117
125
|
|
118
126
|
if expanded_property == '@reverse'
|
119
|
-
compacted_value = compact(expanded_value,
|
120
|
-
|
127
|
+
compacted_value = compact(expanded_value, base: base,
|
128
|
+
property: '@reverse',
|
129
|
+
log_depth: log_depth.to_i + 1)
|
130
|
+
log_debug("@reverse", depth: log_depth.to_i) {"compacted_value: #{compacted_value.inspect}"}
|
121
131
|
# handle double-reversed properties
|
122
132
|
compacted_value.each do |prop, value|
|
123
133
|
if context.reverse?(prop)
|
@@ -128,8 +138,8 @@ module JSON::LD
|
|
128
138
|
end
|
129
139
|
|
130
140
|
unless compacted_value.empty?
|
131
|
-
al = context.compact_iri('@reverse'
|
132
|
-
|
141
|
+
al = context.compact_iri('@reverse')
|
142
|
+
log_debug("", depth: log_depth.to_i) {"remainder: #{al} => #{compacted_value.inspect}"}
|
133
143
|
result[al] = compacted_value
|
134
144
|
end
|
135
145
|
next
|
@@ -137,8 +147,10 @@ module JSON::LD
|
|
137
147
|
|
138
148
|
if expanded_property == '@preserve'
|
139
149
|
# Compact using `property`
|
140
|
-
compacted_value = compact(expanded_value,
|
141
|
-
|
150
|
+
compacted_value = compact(expanded_value, base: base,
|
151
|
+
property: property,
|
152
|
+
log_depth: log_depth.to_i + 1)
|
153
|
+
log_debug("@preserve", depth: log_depth.to_i) {"compacted_value: #{compacted_value.inspect}"}
|
142
154
|
|
143
155
|
unless compacted_value.is_a?(Array) && compacted_value.empty?
|
144
156
|
result['@preserve'] = compacted_value
|
@@ -146,15 +158,15 @@ module JSON::LD
|
|
146
158
|
next
|
147
159
|
end
|
148
160
|
|
149
|
-
if expanded_property == '@index' && context.container(property)
|
150
|
-
|
161
|
+
if expanded_property == '@index' && context.container(property).include?('@index')
|
162
|
+
log_debug("@index", depth: log_depth.to_i) {"drop @index"}
|
151
163
|
next
|
152
164
|
end
|
153
165
|
|
154
166
|
# Otherwise, if expanded property is @direction, @index, @value, or @language:
|
155
167
|
if EXPANDED_PROPERTY_DIRECTION_INDEX_LANGUAGE_VALUE.include?(expanded_property)
|
156
|
-
al = context.compact_iri(expanded_property, vocab: true
|
157
|
-
|
168
|
+
al = context.compact_iri(expanded_property, vocab: true)
|
169
|
+
log_debug(expanded_property, depth: log_depth.to_i) {"#{al} => #{expanded_value.inspect}"}
|
158
170
|
result[al] = expanded_value
|
159
171
|
next
|
160
172
|
end
|
@@ -164,8 +176,7 @@ module JSON::LD
|
|
164
176
|
context.compact_iri(expanded_property,
|
165
177
|
value: expanded_value,
|
166
178
|
vocab: true,
|
167
|
-
reverse: inside_reverse
|
168
|
-
log_depth: @options[:log_depth])
|
179
|
+
reverse: inside_reverse)
|
169
180
|
|
170
181
|
if nest_prop = context.nest(item_active_property)
|
171
182
|
result[nest_prop] ||= {}
|
@@ -183,8 +194,7 @@ module JSON::LD
|
|
183
194
|
context.compact_iri(expanded_property,
|
184
195
|
value: expanded_item,
|
185
196
|
vocab: true,
|
186
|
-
reverse: inside_reverse
|
187
|
-
log_depth: @options[:log_depth])
|
197
|
+
reverse: inside_reverse)
|
188
198
|
|
189
199
|
|
190
200
|
nest_result = if nest_prop = context.nest(item_active_property)
|
@@ -203,17 +213,19 @@ module JSON::LD
|
|
203
213
|
else expanded_item
|
204
214
|
end
|
205
215
|
|
206
|
-
compacted_item = compact(value,
|
207
|
-
|
216
|
+
compacted_item = compact(value, base: base,
|
217
|
+
property: item_active_property,
|
218
|
+
log_depth: log_depth.to_i + 1)
|
219
|
+
log_debug("", depth: log_depth.to_i) {" => compacted key: #{item_active_property.inspect} for #{compacted_item.inspect}"}
|
208
220
|
|
209
221
|
# handle @list
|
210
222
|
if list?(expanded_item)
|
211
223
|
compacted_item = as_array(compacted_item)
|
212
|
-
unless container
|
213
|
-
al = context.compact_iri('@list', vocab: true
|
224
|
+
unless container.include?('@list')
|
225
|
+
al = context.compact_iri('@list', vocab: true)
|
214
226
|
compacted_item = {al => compacted_item}
|
215
227
|
if expanded_item.has_key?('@index')
|
216
|
-
key = context.compact_iri('@index', vocab: true
|
228
|
+
key = context.compact_iri('@index', vocab: true)
|
217
229
|
compacted_item[key] = expanded_item['@index']
|
218
230
|
end
|
219
231
|
else
|
@@ -231,11 +243,11 @@ module JSON::LD
|
|
231
243
|
map_object = nest_result[item_active_property] ||= {}
|
232
244
|
# If there is no @id, create a blank node identifier to use as an index
|
233
245
|
map_key = if container.include?('@id') && expanded_item['@id']
|
234
|
-
context.compact_iri(expanded_item['@id'],
|
246
|
+
context.compact_iri(expanded_item['@id'], base: @options[:base])
|
235
247
|
elsif container.include?('@index') && expanded_item['@index']
|
236
|
-
context.compact_iri(expanded_item['@index'],
|
248
|
+
context.compact_iri(expanded_item['@index'], vocab: true)
|
237
249
|
else
|
238
|
-
context.compact_iri('@none', vocab: true
|
250
|
+
context.compact_iri('@none', vocab: true)
|
239
251
|
end
|
240
252
|
add_value(map_object, map_key, compacted_item,
|
241
253
|
property_is_array: as_array)
|
@@ -243,7 +255,7 @@ module JSON::LD
|
|
243
255
|
# container includes @graph but not @id or @index and value is a simple graph object
|
244
256
|
if compacted_item.is_a?(Array) && compacted_item.length > 1
|
245
257
|
# Mutple objects in the same graph can't be represented directly, as they would be interpreted as two different graphs. Need to wrap in @included.
|
246
|
-
included_key = context.compact_iri('@included', vocab: true)
|
258
|
+
included_key = context.compact_iri('@included', vocab: true)
|
247
259
|
compacted_item = {included_key => compacted_item}
|
248
260
|
end
|
249
261
|
# Drop through, where compacted_item will be added
|
@@ -251,35 +263,34 @@ module JSON::LD
|
|
251
263
|
property_is_array: as_array)
|
252
264
|
else
|
253
265
|
# container does not include @graph or otherwise does not match one of the previous cases, redo compacted_item
|
254
|
-
al = context.compact_iri('@graph', vocab: true
|
266
|
+
al = context.compact_iri('@graph', vocab: true)
|
255
267
|
compacted_item = {al => compacted_item}
|
256
268
|
if expanded_item['@id']
|
257
|
-
al = context.compact_iri('@id', vocab: true
|
258
|
-
compacted_item[al] = context.compact_iri(expanded_item['@id'], vocab: false
|
269
|
+
al = context.compact_iri('@id', vocab: true)
|
270
|
+
compacted_item[al] = context.compact_iri(expanded_item['@id'], vocab: false)
|
259
271
|
end
|
260
272
|
if expanded_item.has_key?('@index')
|
261
|
-
key = context.compact_iri('@index', vocab: true
|
273
|
+
key = context.compact_iri('@index', vocab: true)
|
262
274
|
compacted_item[key] = expanded_item['@index']
|
263
275
|
end
|
264
276
|
add_value(nest_result, item_active_property, compacted_item,
|
265
277
|
property_is_array: as_array)
|
266
278
|
end
|
267
|
-
elsif container.
|
279
|
+
elsif container.intersect?(CONTAINER_MAPPING_LANGUAGE_INDEX_ID_TYPE) && !container.include?('@graph')
|
268
280
|
map_object = nest_result[item_active_property] ||= {}
|
269
281
|
c = container.first
|
270
|
-
container_key = context.compact_iri(c, vocab: true
|
271
|
-
compacted_item = case
|
272
|
-
when
|
282
|
+
container_key = context.compact_iri(c, vocab: true)
|
283
|
+
compacted_item = case
|
284
|
+
when container.include?('@id')
|
273
285
|
map_key = compacted_item[container_key]
|
274
286
|
compacted_item.delete(container_key)
|
275
287
|
compacted_item
|
276
|
-
when
|
288
|
+
when container.include?('@index')
|
277
289
|
index_key = context.term_definitions[item_active_property].index || '@index'
|
278
290
|
if index_key == '@index'
|
279
291
|
map_key = expanded_item['@index']
|
280
|
-
compacted_item.delete(container_key) if compacted_item.is_a?(Hash)
|
281
292
|
else
|
282
|
-
container_key = context.compact_iri(index_key, vocab: true
|
293
|
+
container_key = context.compact_iri(index_key, vocab: true)
|
283
294
|
map_key, *others = Array(compacted_item[container_key])
|
284
295
|
if map_key.is_a?(String)
|
285
296
|
case others.length
|
@@ -288,15 +299,15 @@ module JSON::LD
|
|
288
299
|
else compacted_item[container_key] = others
|
289
300
|
end
|
290
301
|
else
|
291
|
-
map_key = context.compact_iri('@none', vocab: true
|
302
|
+
map_key = context.compact_iri('@none', vocab: true)
|
292
303
|
end
|
293
304
|
end
|
294
305
|
# Note, if compacted_item is a node reference and key is @id-valued, then this could be compacted further.
|
295
306
|
compacted_item
|
296
|
-
when
|
307
|
+
when container.include?('@language')
|
297
308
|
map_key = expanded_item['@language']
|
298
309
|
value?(expanded_item) ? expanded_item['@value'] : compacted_item
|
299
|
-
when
|
310
|
+
when container.include?('@type')
|
300
311
|
map_key, *types = Array(compacted_item[container_key])
|
301
312
|
case types.length
|
302
313
|
when 0 then compacted_item.delete(container_key)
|
@@ -306,11 +317,14 @@ module JSON::LD
|
|
306
317
|
|
307
318
|
# if compacted_item contains a single entry who's key maps to @id, then recompact the item without @type
|
308
319
|
if compacted_item.keys.length == 1 && expanded_item.keys.include?('@id')
|
309
|
-
compacted_item = compact({'@id' => expanded_item['@id']},
|
320
|
+
compacted_item = compact({'@id' => expanded_item['@id']},
|
321
|
+
base: base,
|
322
|
+
property: item_active_property,
|
323
|
+
log_depth: log_depth.to_i + 1)
|
310
324
|
end
|
311
325
|
compacted_item
|
312
326
|
end
|
313
|
-
map_key ||= context.compact_iri('@none', vocab: true
|
327
|
+
map_key ||= context.compact_iri('@none', vocab: true)
|
314
328
|
add_value(map_object, map_key, compacted_item,
|
315
329
|
property_is_array: as_array)
|
316
330
|
else
|
@@ -323,7 +337,7 @@ module JSON::LD
|
|
323
337
|
result
|
324
338
|
else
|
325
339
|
# For other types, the compacted value is the element value
|
326
|
-
|
340
|
+
log_debug("compact", depth: log_depth.to_i) {element.class.to_s}
|
327
341
|
element
|
328
342
|
end
|
329
343
|
|