json-ld 3.2.3 → 3.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/json/ld/api.rb +807 -764
  4. data/lib/json/ld/compact.rb +304 -304
  5. data/lib/json/ld/conneg.rb +179 -161
  6. data/lib/json/ld/context.rb +2080 -1945
  7. data/lib/json/ld/expand.rb +745 -666
  8. data/lib/json/ld/extensions.rb +14 -13
  9. data/lib/json/ld/flatten.rb +257 -247
  10. data/lib/json/ld/format.rb +202 -194
  11. data/lib/json/ld/frame.rb +525 -502
  12. data/lib/json/ld/from_rdf.rb +224 -166
  13. data/lib/json/ld/html/nokogiri.rb +123 -121
  14. data/lib/json/ld/html/rexml.rb +151 -147
  15. data/lib/json/ld/reader.rb +107 -100
  16. data/lib/json/ld/resource.rb +224 -205
  17. data/lib/json/ld/streaming_reader.rb +574 -507
  18. data/lib/json/ld/streaming_writer.rb +93 -92
  19. data/lib/json/ld/to_rdf.rb +171 -167
  20. data/lib/json/ld/utils.rb +270 -264
  21. data/lib/json/ld/version.rb +24 -14
  22. data/lib/json/ld/writer.rb +334 -311
  23. data/lib/json/ld.rb +103 -96
  24. metadata +78 -209
  25. data/spec/api_spec.rb +0 -132
  26. data/spec/compact_spec.rb +0 -3482
  27. data/spec/conneg_spec.rb +0 -373
  28. data/spec/context_spec.rb +0 -2036
  29. data/spec/expand_spec.rb +0 -4496
  30. data/spec/flatten_spec.rb +0 -1203
  31. data/spec/format_spec.rb +0 -115
  32. data/spec/frame_spec.rb +0 -2498
  33. data/spec/from_rdf_spec.rb +0 -1005
  34. data/spec/matchers.rb +0 -20
  35. data/spec/rdfstar_spec.rb +0 -25
  36. data/spec/reader_spec.rb +0 -883
  37. data/spec/resource_spec.rb +0 -76
  38. data/spec/spec_helper.rb +0 -281
  39. data/spec/streaming_reader_spec.rb +0 -237
  40. data/spec/streaming_writer_spec.rb +0 -145
  41. data/spec/suite_compact_spec.rb +0 -22
  42. data/spec/suite_expand_spec.rb +0 -36
  43. data/spec/suite_flatten_spec.rb +0 -34
  44. data/spec/suite_frame_spec.rb +0 -29
  45. data/spec/suite_from_rdf_spec.rb +0 -22
  46. data/spec/suite_helper.rb +0 -411
  47. data/spec/suite_html_spec.rb +0 -22
  48. data/spec/suite_http_spec.rb +0 -35
  49. data/spec/suite_remote_doc_spec.rb +0 -22
  50. data/spec/suite_to_rdf_spec.rb +0 -30
  51. data/spec/support/extensions.rb +0 -44
  52. data/spec/test-files/test-1-compacted.jsonld +0 -10
  53. data/spec/test-files/test-1-context.jsonld +0 -7
  54. data/spec/test-files/test-1-expanded.jsonld +0 -5
  55. data/spec/test-files/test-1-input.jsonld +0 -10
  56. data/spec/test-files/test-1-rdf.ttl +0 -8
  57. data/spec/test-files/test-2-compacted.jsonld +0 -20
  58. data/spec/test-files/test-2-context.jsonld +0 -7
  59. data/spec/test-files/test-2-expanded.jsonld +0 -16
  60. data/spec/test-files/test-2-input.jsonld +0 -20
  61. data/spec/test-files/test-2-rdf.ttl +0 -14
  62. data/spec/test-files/test-3-compacted.jsonld +0 -11
  63. data/spec/test-files/test-3-context.jsonld +0 -8
  64. data/spec/test-files/test-3-expanded.jsonld +0 -10
  65. data/spec/test-files/test-3-input.jsonld +0 -11
  66. data/spec/test-files/test-3-rdf.ttl +0 -8
  67. data/spec/test-files/test-4-compacted.jsonld +0 -10
  68. data/spec/test-files/test-4-context.jsonld +0 -7
  69. data/spec/test-files/test-4-expanded.jsonld +0 -6
  70. data/spec/test-files/test-4-input.jsonld +0 -10
  71. data/spec/test-files/test-4-rdf.ttl +0 -5
  72. data/spec/test-files/test-5-compacted.jsonld +0 -13
  73. data/spec/test-files/test-5-context.jsonld +0 -7
  74. data/spec/test-files/test-5-expanded.jsonld +0 -9
  75. data/spec/test-files/test-5-input.jsonld +0 -13
  76. data/spec/test-files/test-5-rdf.ttl +0 -7
  77. data/spec/test-files/test-6-compacted.jsonld +0 -10
  78. data/spec/test-files/test-6-context.jsonld +0 -7
  79. data/spec/test-files/test-6-expanded.jsonld +0 -10
  80. data/spec/test-files/test-6-input.jsonld +0 -10
  81. data/spec/test-files/test-6-rdf.ttl +0 -6
  82. data/spec/test-files/test-7-compacted.jsonld +0 -23
  83. data/spec/test-files/test-7-context.jsonld +0 -4
  84. data/spec/test-files/test-7-expanded.jsonld +0 -20
  85. data/spec/test-files/test-7-input.jsonld +0 -23
  86. data/spec/test-files/test-7-rdf.ttl +0 -14
  87. data/spec/test-files/test-8-compacted.jsonld +0 -34
  88. data/spec/test-files/test-8-context.jsonld +0 -11
  89. data/spec/test-files/test-8-expanded.jsonld +0 -24
  90. data/spec/test-files/test-8-frame.jsonld +0 -18
  91. data/spec/test-files/test-8-framed.jsonld +0 -25
  92. data/spec/test-files/test-8-input.jsonld +0 -30
  93. data/spec/test-files/test-8-rdf.ttl +0 -15
  94. data/spec/test-files/test-9-compacted.jsonld +0 -20
  95. data/spec/test-files/test-9-context.jsonld +0 -13
  96. data/spec/test-files/test-9-expanded.jsonld +0 -14
  97. data/spec/test-files/test-9-input.jsonld +0 -12
  98. data/spec/to_rdf_spec.rb +0 -1551
  99. data/spec/writer_spec.rb +0 -427
data/lib/json/ld/api.rb CHANGED
@@ -1,5 +1,7 @@
1
- # -*- encoding: utf-8 -*-
2
1
  # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
3
5
  require 'openssl'
4
6
  require 'cgi'
5
7
  require 'json/ld/expand'
@@ -14,879 +16,920 @@ begin
14
16
  rescue LoadError
15
17
  end
16
18
 
17
- module JSON::LD
18
- ##
19
- # A JSON-LD processor based on the JsonLdProcessor interface.
20
- #
21
- # This API provides a clean mechanism that enables developers to convert JSON-LD data into a a variety of output formats that are easier to work with in various programming languages. If a JSON-LD API is provided in a programming environment, the entirety of the following API must be implemented.
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.
24
- #
25
- # @see https://www.w3.org/TR/json-ld11-api/#the-application-programming-interface
26
- # @author [Gregg Kellogg](http://greggkellogg.net/)
27
- class API
28
- include Expand
29
- include Compact
30
- include ToRDF
31
- include Flatten
32
- include FromRDF
33
- include Frame
34
- include RDF::Util::Logger
35
-
36
- # Options used for open_file
37
- OPEN_OPTS = {
38
- headers: {"Accept" => "application/ld+json, text/html;q=0.8, application/xhtml+xml;q=0.8, application/json;q=0.5"}
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
-
47
- # Current input
48
- # @!attribute [rw] input
49
- # @return [String, #read, Hash, Array]
50
- attr_accessor :value
51
-
52
- # Input evaluation context
53
- # @!attribute [rw] context
54
- # @return [JSON::LD::Context]
55
- attr_accessor :context
56
-
57
- # Current Blank Node Namer
58
- # @!attribute [r] namer
59
- # @return [JSON::LD::BlankNodeNamer]
60
- attr_reader :namer
61
-
19
+ module JSON
20
+ module LD
62
21
  ##
63
- # Initialize the API, reading in any document and setting global options
22
+ # A JSON-LD processor based on the JsonLdProcessor interface.
64
23
  #
65
- # @param [String, #read, Hash, Array] input
66
- # @param [String, #read, Hash, Array, JSON::LD::Context] context
67
- # An external context to use additionally to the context embedded in input when expanding the input.
68
- # @param [Hash{Symbol => Object}] options
69
- # @option options [RDF::URI, String, #to_s] :base
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
- # @option options [Boolean] :compactArrays (true)
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.
73
- # @option options [Boolean] :compactToRelative (true)
74
- # Creates document relative IRIs when compacting, if `true`, otherwise leaves expanded.
75
- # @option options [Proc] :documentLoader
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`.
79
- # @option options [String, #read, Hash, Array, JSON::LD::Context] :expandContext
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.
83
- # @option options [Boolean, String, RDF::URI] :flatten
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.
85
- # @option options [String] :language
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.
89
- # @option options [String] :processingMode
90
- # Processing mode, json-ld-1.0 or json-ld-1.1.
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`.
92
- # @option options [Boolean] rdfstar (false)
93
- # support parsing JSON-LD-star statement resources.
94
- # @option options [Boolean] :rename_bnodes (true)
95
- # Rename bnodes as part of expansion, or keep them the same.
96
- # @option options [Boolean] :unique_bnodes (false)
97
- # Use unique bnode identifiers, defaults to using the identifier which the node was originally initialized with (if any).
98
- # @option options [Symbol] :adapter used with MultiJson
99
- # @option options [Boolean] :validate Validate input, if a string or readable object.
100
- # @option options [Boolean] :ordered (true)
101
- # Order traversal of dictionary members by key when performing algorithms.
102
- # @yield [api]
103
- # @yieldparam [API]
104
- # @raise [JsonLdError]
105
- def initialize(input, context, **options, &block)
106
- @options = {
107
- compactArrays: true,
108
- ordered: false,
109
- extractAllScripts: false,
110
- rename_bnodes: true,
111
- unique_bnodes: false,
112
- }.merge(options)
113
- @namer = @options[:unique_bnodes] ? BlankNodeUniqer.new : (@options[:rename_bnodes] ? BlankNodeNamer.new("b") : BlankNodeMapper.new)
114
-
115
- @options[:base] = RDF::URI(@options[:base]) if @options[:base] && !@options[:base].is_a?(RDF::URI)
116
- # For context via Link header
117
- _, context_ref = nil, nil
118
-
119
- @value = case input
120
- when Array, Hash then input.dup
121
- when IO, StringIO, String
122
- remote_doc = self.class.loadRemoteDocument(input, **@options)
123
-
124
- context_ref = remote_doc.contextUrl
125
- @options[:base] = RDF::URI(remote_doc.documentUrl) if remote_doc.documentUrl && !@options[:no_default_base]
126
-
127
- case remote_doc.document
128
- when String
129
- mj_opts = options.keep_if {|k,v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v)}
130
- MultiJson.load(remote_doc.document, **mj_opts)
24
+ # This API provides a clean mechanism that enables developers to convert JSON-LD data into a a variety of output formats that are easier to work with in various programming languages. If a JSON-LD API is provided in a programming environment, the entirety of the following API must be implemented.
25
+ #
26
+ # 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.
27
+ #
28
+ # @see https://www.w3.org/TR/json-ld11-api/#the-application-programming-interface
29
+ # @author [Gregg Kellogg](http://greggkellogg.net/)
30
+ class API
31
+ include Expand
32
+ include Compact
33
+ include ToRDF
34
+ include Flatten
35
+ include FromRDF
36
+ include Frame
37
+ include RDF::Util::Logger
38
+
39
+ # Options used for open_file
40
+ OPEN_OPTS = {
41
+ headers: { "Accept" => "application/ld+json, text/html;q=0.8, application/xhtml+xml;q=0.8, application/json;q=0.5" }
42
+ }
43
+
44
+ # The following constants are used to reduce object allocations
45
+ LINK_REL_CONTEXT = %w[rel http://www.w3.org/ns/json-ld#context].freeze
46
+ LINK_REL_ALTERNATE = %w[rel alternate].freeze
47
+ LINK_TYPE_JSONLD = %w[type application/ld+json].freeze
48
+ JSON_LD_PROCESSING_MODES = %w[json-ld-1.0 json-ld-1.1].freeze
49
+
50
+ # Current input
51
+ # @!attribute [rw] input
52
+ # @return [String, #read, Hash, Array]
53
+ attr_accessor :value
54
+
55
+ # Input evaluation context
56
+ # @!attribute [rw] context
57
+ # @return [JSON::LD::Context]
58
+ attr_accessor :context
59
+
60
+ # Current Blank Node Namer
61
+ # @!attribute [r] namer
62
+ # @return [JSON::LD::BlankNodeNamer]
63
+ attr_reader :namer
64
+
65
+ ##
66
+ # Initialize the API, reading in any document and setting global options
67
+ #
68
+ # @param [String, #read, Hash, Array] input
69
+ # @param [String, #read, Hash, Array, JSON::LD::Context] context
70
+ # An external context to use additionally to the context embedded in input when expanding the input.
71
+ # @param [Hash{Symbol => Object}] options
72
+ # @option options [Symbol] :adapter used with MultiJson
73
+ # @option options [RDF::URI, String, #to_s] :base
74
+ # 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.
75
+ # @option options [Boolean] :compactArrays (true)
76
+ # 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.
77
+ # @option options [Boolean] :compactToRelative (true)
78
+ # Creates document relative IRIs when compacting, if `true`, otherwise leaves expanded.
79
+ # @option options [Proc] :documentLoader
80
+ # 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.
81
+ # @option options [String, #read, Hash, Array, JSON::LD::Context] :expandContext
82
+ # A context that is used to initialize the active context when expanding a document.
83
+ # @option options [Boolean] :extendedRepresentation (false)
84
+ # Use the extended internal representation.
85
+ # @option options [Boolean] :extractAllScripts
86
+ # 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.
87
+ # @option options [Boolean, String, RDF::URI] :flatten
88
+ # 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.
89
+ # @option options [String] :language
90
+ # 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.
91
+ # @option options [Symbol] :library
92
+ # One of :nokogiri or :rexml. If nil/unspecified uses :nokogiri if available, :rexml otherwise.
93
+ # @option options [Boolean] :lowercaseLanguage
94
+ # By default, language tags are left as is. To normalize to lowercase, set this option to `true`.
95
+ # @option options [Boolean] :ordered (true)
96
+ # Order traversal of dictionary members by key when performing algorithms.
97
+ # @option options [String] :processingMode
98
+ # Processing mode, json-ld-1.0 or json-ld-1.1.
99
+ # @option options [Boolean] :rdfstar (false)
100
+ # support parsing JSON-LD-star statement resources.
101
+ # @option options [Boolean] :rename_bnodes (true)
102
+ # Rename bnodes as part of expansion, or keep them the same.
103
+ # @option options [Boolean] :unique_bnodes (false)
104
+ # Use unique bnode identifiers, defaults to using the identifier which the node was originally initialized with (if any).
105
+ # @option options [Boolean] :validate Validate input, if a string or readable object.
106
+ # @yield [api]
107
+ # @yieldparam [API]
108
+ # @raise [JsonLdError]
109
+ def initialize(input, context, **options, &block)
110
+ @options = {
111
+ compactArrays: true,
112
+ ordered: false,
113
+ extractAllScripts: false,
114
+ rename_bnodes: true,
115
+ unique_bnodes: false
116
+ }.merge(options)
117
+ @namer = if @options[:unique_bnodes]
118
+ BlankNodeUniqer.new
131
119
  else
132
- # Already parsed
133
- remote_doc.document
120
+ (@options[:rename_bnodes] ? BlankNodeNamer.new("b") : BlankNodeMapper.new)
134
121
  end
135
- end
136
122
 
137
- # If not provided, first use context from document, or from a Link header
138
- context ||= context_ref || {}
139
- @context = Context.parse(context, **@options)
123
+ @options[:base] = RDF::URI(@options[:base]) if @options[:base] && !@options[:base].is_a?(RDF::URI)
124
+ # For context via Link header
125
+ _ = nil
126
+ context_ref = nil
127
+
128
+ @value = case input
129
+ when Array, Hash then input.dup
130
+ when IO, StringIO, String
131
+ remote_doc = self.class.loadRemoteDocument(input, **@options)
132
+
133
+ context_ref = remote_doc.contextUrl
134
+ @options[:base] = RDF::URI(remote_doc.documentUrl) if remote_doc.documentUrl && !@options[:no_default_base]
135
+
136
+ case remote_doc.document
137
+ when String
138
+ mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
139
+ MultiJson.load(remote_doc.document, **mj_opts)
140
+ else
141
+ # Already parsed
142
+ remote_doc.document
143
+ end
144
+ end
145
+
146
+ # If not provided, first use context from document, or from a Link header
147
+ context ||= context_ref || {}
148
+ @context = Context.parse(context, **@options)
149
+
150
+ return unless block
140
151
 
141
- if block_given?
142
152
  case block.arity
143
- when 0, -1 then instance_eval(&block)
144
- else block.call(self)
153
+ when 0, -1 then instance_eval(&block)
154
+ else yield(self)
145
155
  end
146
156
  end
147
- end
148
157
 
149
- # This is used internally only
150
- private :initialize
151
-
152
- ##
153
- # Expands the given input according to the steps in the Expansion Algorithm. The input must be copied, expanded and returned if there are no errors. If the expansion fails, an appropriate exception must be thrown.
154
- #
155
- # The resulting `Array` either returned or yielded
156
- #
157
- # @param [String, #read, Hash, Array] input
158
- # The JSON-LD object to copy and perform the expansion upon.
159
- # @param [Proc] serializer (nil)
160
- # A Serializer method used for generating the JSON serialization of the result. If absent, the internal Ruby objects are returned, which can be transformed to JSON externally via `#to_json`.
161
- # See {JSON::LD::API.serializer}.
162
- # @param [Hash{Symbol => Object}] options
163
- # @option options (see #initialize)
164
- # @raise [JsonLdError]
165
- # @yield jsonld, base_iri
166
- # @yieldparam [Array<Hash>] jsonld
167
- # The expanded JSON-LD document
168
- # @yieldparam [RDF::URI] base_iri
169
- # The document base as determined during expansion
170
- # @yieldreturn [Object] returned object
171
- # @return [Object, Array<Hash>]
172
- # If a block is given, the result of evaluating the block is returned, otherwise, the expanded JSON-LD document
173
- # @see https://www.w3.org/TR/json-ld11-api/#expansion-algorithm
174
- def self.expand(input, framing: false, serializer: nil, **options, &block)
175
- result = doc_base = nil
176
- API.new(input, options[:expandContext], **options) do
177
- result = self.expand(self.value, nil, self.context,
178
- framing: framing)
179
- doc_base = @options[:base]
180
- end
158
+ # This is used internally only
159
+ private :initialize
160
+
161
+ ##
162
+ # Expands the given input according to the steps in the Expansion Algorithm. The input must be copied, expanded and returned if there are no errors. If the expansion fails, an appropriate exception must be thrown.
163
+ #
164
+ # The resulting `Array` either returned or yielded
165
+ #
166
+ # @param [String, #read, Hash, Array] input
167
+ # The JSON-LD object to copy and perform the expansion upon.
168
+ # @param [Proc] serializer (nil)
169
+ # A Serializer method used for generating the JSON serialization of the result. If absent, the internal Ruby objects are returned, which can be transformed to JSON externally via `#to_json`.
170
+ # See {JSON::LD::API.serializer}.
171
+ # @param [Hash{Symbol => Object}] options
172
+ # @option options (see #initialize)
173
+ # @raise [JsonLdError]
174
+ # @yield jsonld, base_iri
175
+ # @yieldparam [Array<Hash>] jsonld
176
+ # The expanded JSON-LD document
177
+ # @yieldparam [RDF::URI] base_iri
178
+ # The document base as determined during expansion
179
+ # @yieldreturn [Object] returned object
180
+ # @return [Object, Array<Hash>]
181
+ # If a block is given, the result of evaluating the block is returned, otherwise, the expanded JSON-LD document
182
+ # @see https://www.w3.org/TR/json-ld11-api/#expansion-algorithm
183
+ def self.expand(input, framing: false, serializer: nil, **options, &block)
184
+ result = doc_base = nil
185
+ API.new(input, options[:expandContext], **options) do
186
+ result = expand(value, nil, context,
187
+ framing: framing)
188
+ doc_base = @options[:base]
189
+ end
181
190
 
182
- # If, after the algorithm outlined above is run, the resulting element is an JSON object with just a @graph property, element is set to the value of @graph's value.
183
- result = result['@graph'] if result.is_a?(Hash) && result.length == 1 && result.key?('@graph')
191
+ # If, after the algorithm outlined above is run, the resulting element is an JSON object with just a @graph property, element is set to the value of @graph's value.
192
+ result = result['@graph'] if result.is_a?(Hash) && result.length == 1 && result.key?('@graph')
184
193
 
185
- # Finally, if element is a JSON object, it is wrapped into an array.
186
- result = [result].compact unless result.is_a?(Array)
187
- result = serializer.call(result, **options) if serializer
194
+ # Finally, if element is a JSON object, it is wrapped into an array.
195
+ result = [result].compact unless result.is_a?(Array)
196
+ result = serializer.call(result, **options) if serializer
188
197
 
189
- if block_given?
190
- case block.arity
191
- when 1 then yield(result)
192
- when 2 then yield(result, doc_base)
198
+ if block
199
+ case block.arity
200
+ when 1 then yield(result)
201
+ when 2 then yield(result, doc_base)
202
+ else
203
+ raise "Unexpected number of yield parameters to expand"
204
+ end
193
205
  else
194
- raise "Unexpected number of yield parameters to expand"
206
+ result
195
207
  end
196
- else
197
- result
198
208
  end
199
- end
200
209
 
201
- ##
202
- # Compacts the given input according to the steps in the Compaction Algorithm. The input must be copied, compacted and returned if there are no errors. If the compaction fails, an appropirate exception must be thrown.
203
- #
204
- # If no context is provided, the input document is compacted using the top-level context of the document
205
- #
206
- # The resulting `Hash` is either returned or yielded, if a block is given.
207
- #
208
- # @param [String, #read, Hash, Array] input
209
- # The JSON-LD object to copy and perform the compaction upon.
210
- # @param [String, #read, Hash, Array, JSON::LD::Context] context
211
- # The base context to use when compacting the input.
212
- # @param [Proc] serializer (nil)
213
- # A Serializer instance used for generating the JSON serialization of the result. If absent, the internal Ruby objects are returned, which can be transformed to JSON externally via `#to_json`.
214
- # See {JSON::LD::API.serializer}.
215
- # @param [Boolean] expanded (false) Input is already expanded
216
- # @param [Hash{Symbol => Object}] options
217
- # @option options (see #initialize)
218
- # @yield jsonld
219
- # @yieldparam [Hash] jsonld
220
- # The compacted JSON-LD document
221
- # @yieldreturn [Object] returned object
222
- # @return [Object, Hash]
223
- # If a block is given, the result of evaluating the block is returned, otherwise, the compacted JSON-LD document
224
- # @raise [JsonLdError]
225
- # @see https://www.w3.org/TR/json-ld11-api/#compaction-algorithm
226
- def self.compact(input, context, expanded: false, serializer: nil, **options)
227
- result = nil
228
- options = {compactToRelative: true}.merge(options)
229
-
230
- # 1) Perform the Expansion Algorithm on the JSON-LD input.
231
- # This removes any existing context to allow the given context to be cleanly applied.
232
- expanded_input = expanded ? input : API.expand(input, ordered: false, **options) do |res, base_iri|
233
- options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
234
- res
235
- end
210
+ ##
211
+ # Compacts the given input according to the steps in the Compaction Algorithm. The input must be copied, compacted and returned if there are no errors. If the compaction fails, an appropirate exception must be thrown.
212
+ #
213
+ # If no context is provided, the input document is compacted using the top-level context of the document
214
+ #
215
+ # The resulting `Hash` is either returned or yielded, if a block is given.
216
+ #
217
+ # @param [String, #read, Hash, Array] input
218
+ # The JSON-LD object to copy and perform the compaction upon.
219
+ # @param [String, #read, Hash, Array, JSON::LD::Context] context
220
+ # The base context to use when compacting the input.
221
+ # @param [Proc] serializer (nil)
222
+ # A Serializer instance used for generating the JSON serialization of the result. If absent, the internal Ruby objects are returned, which can be transformed to JSON externally via `#to_json`.
223
+ # See {JSON::LD::API.serializer}.
224
+ # @param [Boolean] expanded (false) Input is already expanded
225
+ # @param [Hash{Symbol => Object}] options
226
+ # @option options (see #initialize)
227
+ # @yield jsonld
228
+ # @yieldparam [Hash] jsonld
229
+ # The compacted JSON-LD document
230
+ # @yieldreturn [Object] returned object
231
+ # @return [Object, Hash]
232
+ # If a block is given, the result of evaluating the block is returned, otherwise, the compacted JSON-LD document
233
+ # @raise [JsonLdError]
234
+ # @see https://www.w3.org/TR/json-ld11-api/#compaction-algorithm
235
+ def self.compact(input, context, expanded: false, serializer: nil, **options)
236
+ result = nil
237
+ options = { compactToRelative: true }.merge(options)
236
238
 
237
- API.new(expanded_input, context, no_default_base: true, **options) do
238
- log_debug(".compact") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
239
- result = compact(value)
239
+ # 1) Perform the Expansion Algorithm on the JSON-LD input.
240
+ # This removes any existing context to allow the given context to be cleanly applied.
241
+ expanded_input = if expanded
242
+ input
243
+ else
244
+ API.expand(input, ordered: false, **options) do |res, base_iri|
245
+ options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
246
+ res
247
+ end
248
+ end
240
249
 
241
- # xxx) Add the given context to the output
242
- ctx = self.context.serialize(provided_context: context)
243
- if result.is_a?(Array)
244
- kwgraph = self.context.compact_iri('@graph', vocab: true)
245
- result = result.empty? ? {} : {kwgraph => result}
250
+ API.new(expanded_input, context, no_default_base: true, **options) do
251
+ # log_debug(".compact") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
252
+ result = compact(value)
253
+
254
+ # xxx) Add the given context to the output
255
+ ctx = self.context.serialize(provided_context: context)
256
+ if result.is_a?(Array)
257
+ kwgraph = self.context.compact_iri('@graph', vocab: true)
258
+ result = result.empty? ? {} : { kwgraph => result }
259
+ end
260
+ result = ctx.merge(result) unless ctx.fetch('@context', {}).empty?
246
261
  end
247
- result = ctx.merge(result) unless ctx.fetch('@context', {}).empty?
262
+ result = serializer.call(result, **options) if serializer
263
+ block_given? ? yield(result) : result
248
264
  end
249
- result = serializer.call(result, **options) if serializer
250
- block_given? ? yield(result) : result
251
- end
252
265
 
253
- ##
254
- # This algorithm flattens an expanded JSON-LD document by collecting all properties of a node in a single JSON object and labeling all blank nodes with blank node identifiers. This resulting uniform shape of the document, may drastically simplify the code required to process JSON-LD data in certain applications.
255
- #
256
- # The resulting `Array` is either returned, or yielded if a block is given.
257
- #
258
- # @param [String, #read, Hash, Array] input
259
- # The JSON-LD object or array of JSON-LD objects to flatten or an IRI referencing the JSON-LD document to flatten.
260
- # @param [String, #read, Hash, Array, JSON::LD::EvaluationContext] context
261
- # An optional external context to use additionally to the context embedded in input when expanding the input.
262
- # @param [Boolean] expanded (false) Input is already expanded
263
- # @param [Proc] serializer (nil)
264
- # A Serializer instance used for generating the JSON serialization of the result. If absent, the internal Ruby objects are returned, which can be transformed to JSON externally via `#to_json`.
265
- # See {JSON::LD::API.serializer}.
266
- # @param [Hash{Symbol => Object}] options
267
- # @option options (see #initialize)
268
- # @option options [Boolean] :createAnnotations
269
- # Unfold embedded nodes which can be represented using `@annotation`.
270
- # @yield jsonld
271
- # @yieldparam [Hash] jsonld
272
- # The flattened JSON-LD document
273
- # @yieldreturn [Object] returned object
274
- # @return [Object, Hash]
275
- # If a block is given, the result of evaluating the block is returned, otherwise, the flattened JSON-LD document
276
- # @see https://www.w3.org/TR/json-ld11-api/#framing-algorithm
277
- def self.flatten(input, context, expanded: false, serializer: nil, **options)
278
- flattened = []
279
- options = {
280
- compactToRelative: true,
281
- extractAllScripts: true,
282
- }.merge(options)
283
-
284
- # Expand input to simplify processing
285
- expanded_input = expanded ? input : API.expand(input, **options) do |result, base_iri|
286
- options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
287
- result
288
- end
266
+ ##
267
+ # This algorithm flattens an expanded JSON-LD document by collecting all properties of a node in a single JSON object and labeling all blank nodes with blank node identifiers. This resulting uniform shape of the document, may drastically simplify the code required to process JSON-LD data in certain applications.
268
+ #
269
+ # The resulting `Array` is either returned, or yielded if a block is given.
270
+ #
271
+ # @param [String, #read, Hash, Array] input
272
+ # The JSON-LD object or array of JSON-LD objects to flatten or an IRI referencing the JSON-LD document to flatten.
273
+ # @param [String, #read, Hash, Array, JSON::LD::EvaluationContext] context
274
+ # An optional external context to use additionally to the context embedded in input when expanding the input.
275
+ # @param [Boolean] expanded (false) Input is already expanded
276
+ # @param [Proc] serializer (nil)
277
+ # A Serializer instance used for generating the JSON serialization of the result. If absent, the internal Ruby objects are returned, which can be transformed to JSON externally via `#to_json`.
278
+ # See {JSON::LD::API.serializer}.
279
+ # @param [Hash{Symbol => Object}] options
280
+ # @option options (see #initialize)
281
+ # @option options [Boolean] :createAnnotations
282
+ # Unfold embedded nodes which can be represented using `@annotation`.
283
+ # @yield jsonld
284
+ # @yieldparam [Hash] jsonld
285
+ # The flattened JSON-LD document
286
+ # @yieldreturn [Object] returned object
287
+ # @return [Object, Hash]
288
+ # If a block is given, the result of evaluating the block is returned, otherwise, the flattened JSON-LD document
289
+ # @see https://www.w3.org/TR/json-ld11-api/#framing-algorithm
290
+ def self.flatten(input, context, expanded: false, serializer: nil, **options)
291
+ flattened = []
292
+ options = {
293
+ compactToRelative: true,
294
+ extractAllScripts: true
295
+ }.merge(options)
296
+
297
+ # Expand input to simplify processing
298
+ expanded_input = if expanded
299
+ input
300
+ else
301
+ API.expand(input, **options) do |result, base_iri|
302
+ options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
303
+ result
304
+ end
305
+ end
289
306
 
290
- # Initialize input using
291
- API.new(expanded_input, context, no_default_base: true, **options) do
292
- log_debug(".flatten") {"expanded input: #{value.to_json(JSON_STATE) rescue 'malformed json'}"}
307
+ # Initialize input using
308
+ API.new(expanded_input, context, no_default_base: true, **options) do
309
+ # log_debug(".flatten") {"expanded input: #{value.to_json(JSON_STATE) rescue 'malformed json'}"}
293
310
 
294
- # Rename blank nodes recusively. Note that this does not create new blank node identifiers where none exist, which is performed in the node map generation algorithm.
295
- @value = rename_bnodes(@value) if @options[:rename_bnodes]
311
+ # Rename blank nodes recusively. Note that this does not create new blank node identifiers where none exist, which is performed in the node map generation algorithm.
312
+ @value = rename_bnodes(@value) if @options[:rename_bnodes]
296
313
 
297
- # Initialize node map to a JSON object consisting of a single member whose key is @default and whose value is an empty JSON object.
298
- graph_maps = {'@default' => {}}
299
- create_node_map(value, graph_maps)
314
+ # Initialize node map to a JSON object consisting of a single member whose key is @default and whose value is an empty JSON object.
315
+ graph_maps = { '@default' => {} }
316
+ create_node_map(value, graph_maps)
300
317
 
301
- # If create annotations flag is set, then update each node map in graph maps with the result of calling the create annotations algorithm.
302
- if options[:createAnnotations]
303
- graph_maps.values.each do |node_map|
304
- create_annotations(node_map)
318
+ # If create annotations flag is set, then update each node map in graph maps with the result of calling the create annotations algorithm.
319
+ if options[:createAnnotations]
320
+ graph_maps.each_value do |node_map|
321
+ create_annotations(node_map)
322
+ end
305
323
  end
306
- end
307
324
 
308
- default_graph = graph_maps['@default']
309
- graph_maps.keys.opt_sort(ordered: @options[:ordered]).each do |graph_name|
310
- next if graph_name == '@default'
325
+ default_graph = graph_maps['@default']
326
+ graph_maps.keys.opt_sort(ordered: @options[:ordered]).each do |graph_name|
327
+ next if graph_name == '@default'
311
328
 
312
- graph = graph_maps[graph_name]
313
- entry = default_graph[graph_name] ||= {'@id' => graph_name}
314
- nodes = entry['@graph'] ||= []
315
- graph.keys.opt_sort(ordered: @options[:ordered]).each do |id|
316
- nodes << graph[id] unless node_reference?(graph[id])
329
+ graph = graph_maps[graph_name]
330
+ entry = default_graph[graph_name] ||= { '@id' => graph_name }
331
+ nodes = entry['@graph'] ||= []
332
+ graph.keys.opt_sort(ordered: @options[:ordered]).each do |id|
333
+ nodes << graph[id] unless node_reference?(graph[id])
334
+ end
335
+ end
336
+ default_graph.keys.opt_sort(ordered: @options[:ordered]).each do |id|
337
+ flattened << default_graph[id] unless node_reference?(default_graph[id])
317
338
  end
318
- end
319
- default_graph.keys.opt_sort(ordered: @options[:ordered]).each do |id|
320
- flattened << default_graph[id] unless node_reference?(default_graph[id])
321
- end
322
339
 
323
- if context && !flattened.empty?
324
- # 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.
325
- compacted = as_array(compact(flattened))
326
- kwgraph = self.context.compact_iri('@graph', vocab: true)
327
- flattened = self.context.
328
- serialize(provided_context: context).
329
- merge(kwgraph => compacted)
340
+ if context && !flattened.empty?
341
+ # 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.
342
+ compacted = as_array(compact(flattened))
343
+ kwgraph = self.context.compact_iri('@graph', vocab: true)
344
+ flattened = self.context
345
+ .serialize(provided_context: context)
346
+ .merge(kwgraph => compacted)
347
+ end
330
348
  end
331
- end
332
349
 
333
- flattened = serializer.call(flattened, **options) if serializer
334
- block_given? ? yield(flattened) : flattened
335
- end
350
+ flattened = serializer.call(flattened, **options) if serializer
351
+ block_given? ? yield(flattened) : flattened
352
+ end
336
353
 
337
- ##
338
- # Frames the given input using the frame according to the steps in the Framing Algorithm. The input is used to build the framed output and is returned if there are no errors. If there are no matches for the frame, null must be returned. Exceptions must be thrown if there are errors.
339
- #
340
- # The resulting `Array` is either returned, or yielded if a block is given.
341
- #
342
- # @param [String, #read, Hash, Array] input
343
- # The JSON-LD object to copy and perform the framing on.
344
- # @param [String, #read, Hash, Array] frame
345
- # The frame to use when re-arranging the data.
346
- # @param [Boolean] expanded (false) Input is already expanded
347
- # @option options (see #initialize)
348
- # @option options ['@always', '@link', '@once', '@never'] :embed ('@once')
349
- # a flag specifying that objects should be directly embedded in the output, instead of being referred to by their IRI.
350
- # @option options [Boolean] :explicit (false)
351
- # a flag specifying that for properties to be included in the output, they must be explicitly declared in the framing context.
352
- # @option options [Boolean] :requireAll (false)
353
- # 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.
354
- # @option options [Boolean] :omitDefault (false)
355
- # a flag specifying that properties that are missing from the JSON-LD input should be omitted from the output.
356
- # @option options [Boolean] :pruneBlankNodeIdentifiers (true) removes blank node identifiers that are only used once.
357
- # @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`.
358
- # @yield jsonld
359
- # @yieldparam [Hash] jsonld
360
- # The framed JSON-LD document
361
- # @yieldreturn [Object] returned object
362
- # @return [Object, Hash]
363
- # If a block is given, the result of evaluating the block is returned, otherwise, the framed JSON-LD document
364
- # @raise [InvalidFrame]
365
- # @see https://www.w3.org/TR/json-ld11-api/#framing-algorithm
366
- def self.frame(input, frame, expanded: false, serializer: nil, **options)
367
- result = nil
368
- options = {
369
- base: (RDF::URI(input) if input.is_a?(String)),
370
- compactArrays: true,
371
- compactToRelative: true,
372
- embed: '@once',
373
- explicit: false,
374
- requireAll: false,
375
- omitDefault: false,
376
- }.merge(options)
377
-
378
- framing_state = {
379
- graphMap: {},
380
- graphStack: [],
381
- subjectStack: [],
382
- link: {},
383
- embedded: false # False at the top-level
384
- }
354
+ ##
355
+ # Frames the given input using the frame according to the steps in the Framing Algorithm. The input is used to build the framed output and is returned if there are no errors. If there are no matches for the frame, null must be returned. Exceptions must be thrown if there are errors.
356
+ #
357
+ # The resulting `Array` is either returned, or yielded if a block is given.
358
+ #
359
+ # @param [String, #read, Hash, Array] input
360
+ # The JSON-LD object to copy and perform the framing on.
361
+ # @param [String, #read, Hash, Array] frame
362
+ # The frame to use when re-arranging the data.
363
+ # @param [Boolean] expanded (false) Input is already expanded
364
+ # @option options (see #initialize)
365
+ # @option options ['@always', '@link', '@once', '@never'] :embed ('@once')
366
+ # a flag specifying that objects should be directly embedded in the output, instead of being referred to by their IRI.
367
+ # @option options [Boolean] :explicit (false)
368
+ # a flag specifying that for properties to be included in the output, they must be explicitly declared in the framing context.
369
+ # @option options [Boolean] :requireAll (false)
370
+ # 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.
371
+ # @option options [Boolean] :omitDefault (false)
372
+ # a flag specifying that properties that are missing from the JSON-LD input should be omitted from the output.
373
+ # @option options [Boolean] :pruneBlankNodeIdentifiers (true) removes blank node identifiers that are only used once.
374
+ # @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`.
375
+ # @yield jsonld
376
+ # @yieldparam [Hash] jsonld
377
+ # The framed JSON-LD document
378
+ # @yieldreturn [Object] returned object
379
+ # @return [Object, Hash]
380
+ # If a block is given, the result of evaluating the block is returned, otherwise, the framed JSON-LD document
381
+ # @raise [InvalidFrame]
382
+ # @see https://www.w3.org/TR/json-ld11-api/#framing-algorithm
383
+ def self.frame(input, frame, expanded: false, serializer: nil, **options)
384
+ result = nil
385
+ options = {
386
+ base: (RDF::URI(input) if input.is_a?(String)),
387
+ compactArrays: true,
388
+ compactToRelative: true,
389
+ embed: '@once',
390
+ explicit: false,
391
+ requireAll: false,
392
+ omitDefault: false
393
+ }.merge(options)
394
+
395
+ framing_state = {
396
+ graphMap: {},
397
+ graphStack: [],
398
+ subjectStack: [],
399
+ link: {},
400
+ embedded: false # False at the top-level
401
+ }
402
+
403
+ # de-reference frame to create the framing object
404
+ frame = case frame
405
+ when Hash then frame.dup
406
+ when IO, StringIO, String
407
+ remote_doc = loadRemoteDocument(frame,
408
+ profile: 'http://www.w3.org/ns/json-ld#frame',
409
+ requestProfile: 'http://www.w3.org/ns/json-ld#frame',
410
+ **options)
411
+ if remote_doc.document.is_a?(String)
412
+ mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
413
+ MultiJson.load(remote_doc.document, **mj_opts)
414
+ else
415
+ remote_doc.document
416
+ end
417
+ end
385
418
 
386
- # de-reference frame to create the framing object
387
- frame = case frame
388
- when Hash then frame.dup
389
- when IO, StringIO, String
390
- remote_doc = loadRemoteDocument(frame,
391
- profile: 'http://www.w3.org/ns/json-ld#frame',
392
- requestProfile: 'http://www.w3.org/ns/json-ld#frame',
393
- **options)
394
- if remote_doc.document.is_a?(String)
395
- mj_opts = options.keep_if {|k,v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v)}
396
- MultiJson.load(remote_doc.document, **mj_opts)
419
+ # Expand input to simplify processing
420
+ expanded_input = if expanded
421
+ input
397
422
  else
398
- remote_doc.document
423
+ API.expand(input, ordered: false, **options) do |res, base_iri|
424
+ options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
425
+ res
426
+ end
399
427
  end
400
- end
401
428
 
402
- # Expand input to simplify processing
403
- expanded_input = expanded ? input : API.expand(input, ordered: false, **options) do |res, base_iri|
404
- options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
405
- res
406
- end
429
+ # Expand frame to simplify processing
430
+ expanded_frame = API.expand(frame, framing: true, ordered: false, **options)
407
431
 
408
- # Expand frame to simplify processing
409
- expanded_frame = API.expand(frame, framing: true, ordered: false, **options)
432
+ # Initialize input using frame as context
433
+ API.new(expanded_input, frame['@context'], no_default_base: true, **options) do
434
+ # log_debug(".frame") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
435
+ # log_debug(".frame") {"expanded frame: #{expanded_frame.to_json(JSON_STATE) rescue 'malformed json'}"}
410
436
 
411
- # Initialize input using frame as context
412
- API.new(expanded_input, frame['@context'], no_default_base: true, **options) do
413
- log_debug(".frame") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
414
- log_debug(".frame") {"expanded frame: #{expanded_frame.to_json(JSON_STATE) rescue 'malformed json'}"}
437
+ if %w[@first @last].include?(options[:embed]) && context.processingMode('json-ld-1.1')
438
+ if @options[:validate]
439
+ raise JSON::LD::JsonLdError::InvalidEmbedValue,
440
+ "#{options[:embed]} is not a valid value of @embed in 1.1 mode"
441
+ end
415
442
 
416
- if %w(@first @last).include?(options[:embed]) && context.processingMode('json-ld-1.1')
417
- raise JSON::LD::JsonLdError::InvalidEmbedValue, "#{options[:embed]} is not a valid value of @embed in 1.1 mode" if @options[:validate]
418
- warn "[DEPRECATION] #{options[:embed]} is not a valid value of @embed in 1.1 mode.\n"
419
- end
443
+ warn "[DEPRECATION] #{options[:embed]} is not a valid value of @embed in 1.1 mode.\n"
444
+ end
420
445
 
421
- # Set omitGraph option, if not present, based on processingMode
422
- unless options.key?(:omitGraph)
423
- options[:omitGraph] = context.processingMode('json-ld-1.1')
424
- end
446
+ # Set omitGraph option, if not present, based on processingMode
447
+ options[:omitGraph] = context.processingMode('json-ld-1.1') unless options.key?(:omitGraph)
425
448
 
426
- # Rename blank nodes recusively. Note that this does not create new blank node identifiers where none exist, which is performed in the node map generation algorithm.
427
- @value = rename_bnodes(@value)
449
+ # Rename blank nodes recusively. Note that this does not create new blank node identifiers where none exist, which is performed in the node map generation algorithm.
450
+ @value = rename_bnodes(@value)
428
451
 
429
- # Get framing nodes from expanded input, replacing Blank Node identifiers as necessary
430
- create_node_map(value, framing_state[:graphMap], active_graph: '@default')
452
+ # Get framing nodes from expanded input, replacing Blank Node identifiers as necessary
453
+ create_node_map(value, framing_state[:graphMap], active_graph: '@default')
431
454
 
432
- frame_keys = frame.keys.map {|k| context.expand_iri(k, vocab: true)}
433
- if frame_keys.include?('@graph')
434
- # If frame contains @graph, it matches the default graph.
435
- framing_state[:graph] = '@default'
436
- else
437
- # If frame does not contain @graph used the merged graph.
438
- framing_state[:graph] = '@merged'
439
- framing_state[:link]['@merged'] = {}
440
- framing_state[:graphMap]['@merged'] = merge_node_map_graphs(framing_state[:graphMap])
441
- end
455
+ frame_keys = frame.keys.map { |k| context.expand_iri(k, vocab: true) }
456
+ if frame_keys.include?('@graph')
457
+ # If frame contains @graph, it matches the default graph.
458
+ framing_state[:graph] = '@default'
459
+ else
460
+ # If frame does not contain @graph used the merged graph.
461
+ framing_state[:graph] = '@merged'
462
+ framing_state[:link]['@merged'] = {}
463
+ framing_state[:graphMap]['@merged'] = merge_node_map_graphs(framing_state[:graphMap])
464
+ end
442
465
 
443
- framing_state[:subjects] = framing_state[:graphMap][framing_state[:graph]]
466
+ framing_state[:subjects] = framing_state[:graphMap][framing_state[:graph]]
444
467
 
445
- result = []
446
- frame(framing_state, framing_state[:subjects].keys.opt_sort(ordered: @options[:ordered]), (expanded_frame.first || {}), parent: result, **options)
468
+ result = []
469
+ frame(framing_state, framing_state[:subjects].keys.opt_sort(ordered: @options[:ordered]),
470
+ (expanded_frame.first || {}), parent: result, **options)
447
471
 
448
- # Default to based on processinMode
449
- if !options.key?(:pruneBlankNodeIdentifiers)
450
- options[:pruneBlankNodeIdentifiers] = context.processingMode('json-ld-1.1')
451
- end
472
+ # Default to based on processinMode
473
+ unless options.key?(:pruneBlankNodeIdentifiers)
474
+ options[:pruneBlankNodeIdentifiers] = context.processingMode('json-ld-1.1')
475
+ end
452
476
 
453
- # Count blank node identifiers used in the document, if pruning
454
- if options[:pruneBlankNodeIdentifiers]
455
- bnodes_to_clear = count_blank_node_identifiers(result).collect {|k, v| k if v == 1}.compact
456
- result = prune_bnodes(result, bnodes_to_clear)
457
- end
477
+ # Count blank node identifiers used in the document, if pruning
478
+ if options[:pruneBlankNodeIdentifiers]
479
+ bnodes_to_clear = count_blank_node_identifiers(result).collect { |k, v| k if v == 1 }.compact
480
+ result = prune_bnodes(result, bnodes_to_clear)
481
+ end
458
482
 
459
- # Replace values with `@preserve` with the content of its entry.
460
- result = cleanup_preserve(result)
461
- log_debug(".frame") {"expanded result: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
483
+ # Replace values with `@preserve` with the content of its entry.
484
+ result = cleanup_preserve(result)
485
+ # log_debug(".frame") {"expanded result: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
462
486
 
463
- # Compact result
464
- compacted = compact(result)
487
+ # Compact result
488
+ compacted = compact(result)
465
489
 
466
- # @replace `@null` with nil, compacting arrays
467
- compacted = cleanup_null(compacted)
468
- compacted = [compacted] unless options[:omitGraph] || compacted.is_a?(Array)
490
+ # @replace `@null` with nil, compacting arrays
491
+ compacted = cleanup_null(compacted)
492
+ compacted = [compacted] unless options[:omitGraph] || compacted.is_a?(Array)
469
493
 
470
- # Add the given context to the output
471
- result = if !compacted.is_a?(Array)
472
- compacted
473
- else
474
- kwgraph = context.compact_iri('@graph', vocab: true)
475
- {kwgraph => compacted}
494
+ # Add the given context to the output
495
+ result = if compacted.is_a?(Array)
496
+ kwgraph = context.compact_iri('@graph', vocab: true)
497
+ { kwgraph => compacted }
498
+ else
499
+ compacted
500
+ end
501
+ # Only add context if one was provided
502
+ result = context.serialize(provided_context: frame).merge(result) if frame['@context']
503
+
504
+ # log_debug(".frame") {"after compact: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
505
+ result
476
506
  end
477
- # Only add context if one was provided
478
- result = context.serialize(provided_context: frame).merge(result) if frame['@context']
479
-
480
- log_debug(".frame") {"after compact: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
481
- result
482
- end
483
507
 
484
- result = serializer.call(result, **options) if serializer
485
- block_given? ? yield(result) : result
486
- end
508
+ result = serializer.call(result, **options) if serializer
509
+ block_given? ? yield(result) : result
510
+ end
487
511
 
488
- ##
489
- # Processes the input according to the RDF Conversion Algorithm, calling the provided callback for each triple generated.
490
- #
491
- # @param [String, #read, Hash, Array] input
492
- # The JSON-LD object to process when outputting statements.
493
- # @param [Boolean] expanded (false) Input is already expanded
494
- # @option options (see #initialize)
495
- # @option options [Boolean] :produceGeneralizedRdf (false)
496
- # If true, output will include statements having blank node predicates, otherwise they are dropped.
497
- # @raise [JsonLdError]
498
- # @yield statement
499
- # @yieldparam [RDF::Statement] statement
500
- # @return [RDF::Enumerable] set of statements, unless a block is given.
501
- def self.toRdf(input, expanded: false, **options, &block)
502
- unless block_given?
503
- results = []
504
- results.extend(RDF::Enumerable)
505
- self.toRdf(input, expanded: expanded, **options) do |stmt|
506
- results << stmt
512
+ ##
513
+ # Processes the input according to the RDF Conversion Algorithm, calling the provided callback for each triple generated.
514
+ #
515
+ # @param [String, #read, Hash, Array] input
516
+ # The JSON-LD object to process when outputting statements.
517
+ # @param [Boolean] expanded (false) Input is already expanded
518
+ # @option options (see #initialize)
519
+ # @option options [Boolean] :produceGeneralizedRdf (false)
520
+ # If true, output will include statements having blank node predicates, otherwise they are dropped.
521
+ # @raise [JsonLdError]
522
+ # @yield statement
523
+ # @yieldparam [RDF::Statement] statement
524
+ # @return [RDF::Enumerable] set of statements, unless a block is given.
525
+ def self.toRdf(input, expanded: false, **options)
526
+ unless block_given?
527
+ results = []
528
+ results.extend(RDF::Enumerable)
529
+ toRdf(input, expanded: expanded, **options) do |stmt|
530
+ results << stmt
531
+ end
532
+ return results
507
533
  end
508
- return results
509
- end
510
534
 
511
- options = {
512
- extractAllScripts: true,
513
- }.merge(options)
535
+ options = {
536
+ extractAllScripts: true
537
+ }.merge(options)
514
538
 
515
- # Flatten input to simplify processing
516
- flattened_input = API.flatten(input, nil, expanded: expanded, ordered: false, **options)
539
+ # Flatten input to simplify processing
540
+ flattened_input = API.flatten(input, nil, expanded: expanded, ordered: false, **options)
517
541
 
518
- API.new(flattened_input, nil, **options) do
519
- # 1) Perform the Expansion Algorithm on the JSON-LD input.
520
- # This removes any existing context to allow the given context to be cleanly applied.
521
- log_debug(".toRdf") {"flattened input: #{flattened_input.to_json(JSON_STATE) rescue 'malformed json'}"}
542
+ API.new(flattened_input, nil, **options) do
543
+ # 1) Perform the Expansion Algorithm on the JSON-LD input.
544
+ # This removes any existing context to allow the given context to be cleanly applied.
545
+ # log_debug(".toRdf") {"flattened input: #{flattened_input.to_json(JSON_STATE) rescue 'malformed json'}"}
522
546
 
523
- # Recurse through input
524
- flattened_input.each do |node|
525
- item_to_rdf(node) do |statement|
526
- next if statement.predicate.node? && !options[:produceGeneralizedRdf]
547
+ # Recurse through input
548
+ flattened_input.each do |node|
549
+ item_to_rdf(node) do |statement|
550
+ next if statement.predicate.node? && !options[:produceGeneralizedRdf]
527
551
 
528
- # Drop invalid statements (other than IRIs)
529
- unless statement.valid_extended?
530
- log_debug(".toRdf") {"drop invalid statement: #{statement.to_nquads}"}
531
- next
532
- end
552
+ # Drop invalid statements (other than IRIs)
553
+ unless statement.valid_extended?
554
+ # log_debug(".toRdf") {"drop invalid statement: #{statement.to_nquads}"}
555
+ next
556
+ end
533
557
 
534
- yield statement
558
+ yield statement
559
+ end
535
560
  end
536
561
  end
537
562
  end
538
- end
539
-
540
- ##
541
- # Take an ordered list of RDF::Statements and turn them into a JSON-LD document.
542
- #
543
- # The resulting `Array` is either returned or yielded, if a block is given.
544
- #
545
- # @param [RDF::Enumerable] input
546
- # @param [Boolean] useRdfType (false)
547
- # If set to `true`, the JSON-LD processor will treat `rdf:type` like a normal property instead of using `@type`.
548
- # @param [Boolean] useNativeTypes (false) use native representations
549
- # @param [Proc] serializer (nil)
550
- # A Serializer instance used for generating the JSON serialization of the result. If absent, the internal Ruby objects are returned, which can be transformed to JSON externally via `#to_json`.
551
- # See {JSON::LD::API.serializer}.
552
- # @param [Hash{Symbol => Object}] options
553
- # @option options (see #initialize)
554
- # @yield jsonld
555
- # @yieldparam [Hash] jsonld
556
- # The JSON-LD document in expanded form
557
- # @yieldreturn [Object] returned object
558
- # @return [Object, Hash]
559
- # If a block is given, the result of evaluating the block is returned, otherwise, the expanded JSON-LD document
560
- def self.fromRdf(input, useRdfType: false, useNativeTypes: false, serializer: nil, **options, &block)
561
- result = nil
562
-
563
- API.new(nil, nil, **options) do
564
- result = from_statements(input,
565
- useRdfType: useRdfType,
566
- useNativeTypes: useNativeTypes)
567
- end
568
563
 
569
- result = serializer.call(result, **options) if serializer
570
- block_given? ? yield(result) : result
571
- end
564
+ ##
565
+ # Take an ordered list of RDF::Statements and turn them into a JSON-LD document.
566
+ #
567
+ # The resulting `Array` is either returned or yielded, if a block is given.
568
+ #
569
+ # @param [RDF::Enumerable] input
570
+ # @param [Boolean] useRdfType (false)
571
+ # If set to `true`, the JSON-LD processor will treat `rdf:type` like a normal property instead of using `@type`.
572
+ # @param [Boolean] useNativeTypes (false) use native representations
573
+ # @param [Proc] serializer (nil)
574
+ # A Serializer instance used for generating the JSON serialization of the result. If absent, the internal Ruby objects are returned, which can be transformed to JSON externally via `#to_json`.
575
+ # See {JSON::LD::API.serializer}.
576
+ # @param [Hash{Symbol => Object}] options
577
+ # @option options (see #initialize)
578
+ # @yield jsonld
579
+ # @yieldparam [Hash] jsonld
580
+ # The JSON-LD document in expanded form
581
+ # @yieldreturn [Object] returned object
582
+ # @return [Object, Hash]
583
+ # If a block is given, the result of evaluating the block is returned, otherwise, the expanded JSON-LD document
584
+ def self.fromRdf(input, useRdfType: false, useNativeTypes: false, serializer: nil, **options)
585
+ result = nil
586
+
587
+ API.new(nil, nil, **options) do
588
+ result = from_statements(input,
589
+ extendedRepresentation: options[:extendedRepresentation],
590
+ useRdfType: useRdfType,
591
+ useNativeTypes: useNativeTypes)
592
+ end
572
593
 
573
- ##
574
- # Uses built-in or provided documentLoader to retrieve a parsed document.
575
- #
576
- # @param [RDF::URI, String] url
577
- # @param [String, RDF::URI] base
578
- # Location to use as documentUrl instead of `url`.
579
- # @option options [Proc] :documentLoader
580
- # The callback of the loader to be used to retrieve remote documents and contexts.
581
- # @param [Boolean] extractAllScripts
582
- # 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.
583
- # @param [String] profile
584
- # 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.
585
- # @param [String] requestProfile
586
- # One or more IRIs to use in the request as a profile parameter.
587
- # @param [Boolean] validate (false)
588
- # Allow only appropriate content types
589
- # @param [Hash<Symbol => Object>] options
590
- # @yield remote_document
591
- # @yieldparam [RemoteDocumentRemoteDocument, RDF::Util::File::RemoteDocument] remote_document
592
- # @yieldreturn [Object] returned object
593
- # @return [Object, RemoteDocument]
594
- # If a block is given, the result of evaluating the block is returned, otherwise, the retrieved remote document and context information unless block given
595
- # @raise [JsonLdError]
596
- def self.loadRemoteDocument(url,
597
- base: nil,
598
- documentLoader: nil,
599
- extractAllScripts: false,
600
- profile: nil,
601
- requestProfile: nil,
602
- validate: false,
603
- **options)
604
- documentLoader ||= self.method(:documentLoader)
605
- options = OPEN_OPTS.merge(options)
606
- if requestProfile
607
- # Add any request profile
608
- options[:headers]['Accept'] = options[:headers]['Accept'].sub('application/ld+json,', "application/ld+json;profile=#{requestProfile}, application/ld+json;q=0.9,")
594
+ result = serializer.call(result, **options) if serializer
595
+ block_given? ? yield(result) : result
609
596
  end
610
- documentLoader.call(url, **options) do |remote_doc|
611
- case remote_doc
612
- when RDF::Util::File::RemoteDocument
613
- # Convert to RemoteDocument
614
- context_url = if remote_doc.content_type != 'application/ld+json' &&
615
- (remote_doc.content_type == 'application/json' ||
616
- remote_doc.content_type.to_s.match?(%r(application/\w+\+json)))
617
- # Get context link(s)
618
- # Note, we can't simply use #find_link, as we need to detect multiple
619
- links = remote_doc.links.links.select do |link|
620
- link.attr_pairs.include?(LINK_REL_CONTEXT)
597
+
598
+ ##
599
+ # Uses built-in or provided documentLoader to retrieve a parsed document.
600
+ #
601
+ # @param [RDF::URI, String] url
602
+ # @param [Regexp] allowed_content_types
603
+ # A regular expression matching other content types allowed
604
+ # beyond types for JSON and HTML.
605
+ # @param [String, RDF::URI] base
606
+ # Location to use as documentUrl instead of `url`.
607
+ # @option options [Proc] :documentLoader
608
+ # The callback of the loader to be used to retrieve remote documents and contexts.
609
+ # @param [Boolean] extractAllScripts
610
+ # 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.
611
+ # @param [String] profile
612
+ # 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.
613
+ # @param [String] requestProfile
614
+ # One or more IRIs to use in the request as a profile parameter.
615
+ # @param [Boolean] validate (false)
616
+ # Allow only appropriate content types
617
+ # @param [Hash<Symbol => Object>] options
618
+ # @yield remote_document
619
+ # @yieldparam [RemoteDocumentRemoteDocument, RDF::Util::File::RemoteDocument] remote_document
620
+ # @yieldreturn [Object] returned object
621
+ # @return [Object, RemoteDocument]
622
+ # If a block is given, the result of evaluating the block is returned, otherwise, the retrieved remote document and context information unless block given
623
+ # @raise [JsonLdError]
624
+ def self.loadRemoteDocument(url,
625
+ allowed_content_types: nil,
626
+ base: nil,
627
+ documentLoader: nil,
628
+ extractAllScripts: false,
629
+ profile: nil,
630
+ requestProfile: nil,
631
+ validate: false,
632
+ **options)
633
+ documentLoader ||= method(:documentLoader)
634
+ options = OPEN_OPTS.merge(options)
635
+ if requestProfile
636
+ # Add any request profile
637
+ options[:headers]['Accept'] =
638
+ options[:headers]['Accept'].sub('application/ld+json,',
639
+ "application/ld+json;profile=#{requestProfile}, application/ld+json;q=0.9,")
640
+ end
641
+ documentLoader.call(url, **options) do |remote_doc|
642
+ case remote_doc
643
+ when RDF::Util::File::RemoteDocument
644
+ # Convert to RemoteDocument
645
+ context_url = if remote_doc.content_type != 'application/ld+json' &&
646
+ (remote_doc.content_type == 'application/json' ||
647
+ remote_doc.content_type.to_s.match?(%r{application/\w+\+json}))
648
+ # Get context link(s)
649
+ # Note, we can't simply use #find_link, as we need to detect multiple
650
+ links = remote_doc.links.links.select do |link|
651
+ link.attr_pairs.include?(LINK_REL_CONTEXT)
652
+ end
653
+ if links.length > 1
654
+ raise JSON::LD::JsonLdError::MultipleContextLinkHeaders,
655
+ "expected at most 1 Link header with rel=jsonld:context, got #{links.length}"
656
+ end
657
+ Array(links.first).first
621
658
  end
622
- raise JSON::LD::JsonLdError::MultipleContextLinkHeaders,
623
- "expected at most 1 Link header with rel=jsonld:context, got #{links.length}" if links.length > 1
624
- Array(links.first).first
625
- end
626
659
 
627
- # 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
628
- alternate = !remote_doc.content_type.match?(%r(application/(\w*\+)?json)) && remote_doc.links.links.detect do |link|
629
- link.attr_pairs.include?(LINK_REL_ALTERNATE) &&
630
- link.attr_pairs.include?(LINK_TYPE_JSONLD)
631
- end
660
+ # 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
661
+ alternate = !remote_doc.content_type.match?(%r{application/(\w*\+)?json}) && remote_doc.links.links.detect do |link|
662
+ link.attr_pairs.include?(LINK_REL_ALTERNATE) &&
663
+ link.attr_pairs.include?(LINK_TYPE_JSONLD)
664
+ end
632
665
 
633
- remote_doc = if alternate
634
- # Load alternate relative to URL
635
- loadRemoteDocument(RDF::URI(url).join(alternate.href),
666
+ remote_doc = if alternate
667
+ # Load alternate relative to URL
668
+ loadRemoteDocument(RDF::URI(url).join(alternate.href),
636
669
  extractAllScripts: extractAllScripts,
637
670
  profile: profile,
638
671
  requestProfile: requestProfile,
639
672
  validate: validate,
640
673
  base: base,
641
- **options)
674
+ **options)
675
+ else
676
+ RemoteDocument.new(remote_doc.read,
677
+ documentUrl: remote_doc.base_uri,
678
+ contentType: remote_doc.content_type,
679
+ contextUrl: context_url)
680
+ end
681
+ when RemoteDocument
682
+ # Pass through
642
683
  else
643
- RemoteDocument.new(remote_doc.read,
644
- documentUrl: remote_doc.base_uri,
645
- contentType: remote_doc.content_type,
646
- contextUrl: context_url)
684
+ raise JSON::LD::JsonLdError::LoadingDocumentFailed,
685
+ "unknown result from documentLoader: #{remote_doc.class}"
647
686
  end
648
- when RemoteDocument
649
- # Pass through
650
- else
651
- raise JSON::LD::JsonLdError::LoadingDocumentFailed, "unknown result from documentLoader: #{remote_doc.class}"
652
- end
653
687
 
654
- # Use specified document location
655
- remote_doc.documentUrl = base if base
656
-
657
- # Parse any HTML
658
- if remote_doc.document.is_a?(String)
659
- remote_doc.document = case remote_doc.contentType
660
- when 'text/html', 'application/xhtml+xml'
661
- load_html(remote_doc.document,
662
- url: remote_doc.documentUrl,
663
- extractAllScripts: extractAllScripts,
664
- profile: profile,
665
- **options) do |base|
666
- remote_doc.documentUrl = base
688
+ # Use specified document location
689
+ remote_doc.documentUrl = base if base
690
+
691
+ # Parse any HTML
692
+ if remote_doc.document.is_a?(String)
693
+ remote_doc.document = case remote_doc.contentType
694
+ when 'text/html', 'application/xhtml+xml'
695
+ load_html(remote_doc.document,
696
+ url: remote_doc.documentUrl,
697
+ extractAllScripts: extractAllScripts,
698
+ profile: profile,
699
+ **options) do |base|
700
+ remote_doc.documentUrl = base
701
+ end
702
+ else
703
+ validate_input(remote_doc.document, url: remote_doc.documentUrl) if validate
704
+ mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
705
+ MultiJson.load(remote_doc.document, **mj_opts)
667
706
  end
668
- else
669
- validate_input(remote_doc.document, url: remote_doc.documentUrl) if validate
670
- mj_opts = options.keep_if {|k,v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v)}
671
- MultiJson.load(remote_doc.document, **mj_opts)
672
707
  end
673
- end
674
708
 
675
- if remote_doc.contentType && validate
676
- raise IOError, "url: #{url}, contentType: #{remote_doc.contentType}" unless
677
- remote_doc.contentType.match?(/application\/(.+\+)?json|text\/html|application\/xhtml\+xml/)
709
+ if remote_doc.contentType && validate && !(remote_doc.contentType.match?(%r{application/(.+\+)?json|text/html|application/xhtml\+xml}) ||
710
+ (allowed_content_types && remote_doc.contentType.match?(allowed_content_types)))
711
+ raise IOError, "url: #{url}, contentType: #{remote_doc.contentType}"
712
+ end
713
+
714
+ block_given? ? yield(remote_doc) : remote_doc
678
715
  end
679
- block_given? ? yield(remote_doc) : remote_doc
716
+ rescue IOError, MultiJson::ParseError => e
717
+ raise JSON::LD::JsonLdError::LoadingDocumentFailed, e.message
680
718
  end
681
- rescue IOError, MultiJson::ParseError => e
682
- raise JSON::LD::JsonLdError::LoadingDocumentFailed, e.message
683
- end
684
719
 
685
- ##
686
- # Default document loader.
687
- # @param [RDF::URI, String] url
688
- # @param [Boolean] extractAllScripts
689
- # 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.
690
- # @param [String] profile
691
- # 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.
692
- # @param [String] requestProfile
693
- # One or more IRIs to use in the request as a profile parameter.
694
- # @param [Hash<Symbol => Object>] options
695
- # @yield remote_document
696
- # @yieldparam [RemoteDocument, RDF::Util::File::RemoteDocument] remote_document
697
- # @raise [IOError]
698
- def self.documentLoader(url, extractAllScripts: false, profile: nil, requestProfile: nil, **options, &block)
699
- case url
700
- when IO, StringIO
701
- base_uri = options[:base]
702
- base_uri ||= url.base_uri if url.respond_to?(:base_uri)
703
- content_type = options[:content_type]
704
- content_type ||= url.content_type if url.respond_to?(:content_type)
705
- context_url = if url.respond_to?(:links) && url.links &&
706
- (content_type == 'application/json' || content_type.match?(%r(application/(^ld)+json)))
707
- link = url.links.find_link(LINK_REL_CONTEXT)
708
- link.href if link
709
- end
720
+ ##
721
+ # Default document loader.
722
+ # @param [RDF::URI, String] url
723
+ # @param [Boolean] extractAllScripts
724
+ # 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.
725
+ # @param [String] profile
726
+ # 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.
727
+ # @param [String] requestProfile
728
+ # One or more IRIs to use in the request as a profile parameter.
729
+ # @param [Hash<Symbol => Object>] options
730
+ # @yield remote_document
731
+ # @yieldparam [RemoteDocument, RDF::Util::File::RemoteDocument] remote_document
732
+ # @raise [IOError]
733
+ def self.documentLoader(url, extractAllScripts: false, profile: nil, requestProfile: nil, **options, &block)
734
+ case url
735
+ when IO, StringIO
736
+ base_uri = options[:base]
737
+ base_uri ||= url.base_uri if url.respond_to?(:base_uri)
738
+ content_type = options[:content_type]
739
+ content_type ||= url.content_type if url.respond_to?(:content_type)
740
+ context_url = if url.respond_to?(:links) && url.links &&
741
+ (content_type == 'application/json' || content_type.match?(%r{application/(^ld)+json}))
742
+ link = url.links.find_link(LINK_REL_CONTEXT)
743
+ link&.href
744
+ end
710
745
 
711
- block.call(RemoteDocument.new(url.read,
712
- documentUrl: base_uri,
713
- contentType: content_type,
714
- contextUrl: context_url))
715
- else
716
- RDF::Util::File.open_file(url, **options, &block)
746
+ yield(RemoteDocument.new(url.read,
747
+ documentUrl: base_uri,
748
+ contentType: content_type,
749
+ contextUrl: context_url))
750
+ else
751
+ RDF::Util::File.open_file(url, **options, &block)
752
+ end
717
753
  end
718
- end
719
754
 
720
- # Add class method aliases for backwards compatibility
721
- class << self
722
- alias :toRDF :toRdf
723
- alias :fromRDF :fromRdf
724
- end
755
+ # Add class method aliases for backwards compatibility
756
+ class << self
757
+ alias toRDF toRdf
758
+ alias fromRDF fromRdf
759
+ end
725
760
 
726
- ##
727
- # Load one or more script tags from an HTML source.
728
- # Unescapes and uncomments input, returns the internal representation
729
- # Yields document base
730
- # @param [String] input
731
- # @param [String] url Original URL
732
- # @param [:nokogiri, :rexml] library (nil)
733
- # @param [Boolean] extractAllScripts (false)
734
- # @param [Boolean] profile (nil) Optional priortized profile when loading a single script by type.
735
- # @param [Hash{Symbol => Object}] options
736
- def self.load_html(input, url:,
761
+ ##
762
+ # Load one or more script tags from an HTML source.
763
+ # Unescapes and uncomments input, returns the internal representation
764
+ # Yields document base
765
+ # @param [String] input
766
+ # @param [String] url Original URL
767
+ # @param [:nokogiri, :rexml] library (nil)
768
+ # @param [Boolean] extractAllScripts (false)
769
+ # @param [Boolean] profile (nil) Optional priortized profile when loading a single script by type.
770
+ # @param [Hash{Symbol => Object}] options
771
+ def self.load_html(input, url:,
737
772
  library: nil,
738
773
  extractAllScripts: false,
739
774
  profile: nil,
740
775
  **options)
741
776
 
742
- if input.is_a?(String)
743
- library ||= begin
744
- require 'nokogiri'
745
- :nokogiri
746
- rescue LoadError
747
- :rexml
748
- end
749
- require "json/ld/html/#{library}"
777
+ if input.is_a?(String)
778
+ library ||= begin
779
+ require 'nokogiri'
780
+ :nokogiri
781
+ rescue LoadError
782
+ :rexml
783
+ end
784
+ require "json/ld/html/#{library}"
750
785
 
751
- # Parse HTML using the appropriate library
752
- implementation = case library
753
- when :nokogiri then Nokogiri
754
- when :rexml then REXML
755
- end
756
- self.extend(implementation)
786
+ # Parse HTML using the appropriate library
787
+ implementation = case library
788
+ when :nokogiri then Nokogiri
789
+ when :rexml then REXML
790
+ end
791
+ extend(implementation)
757
792
 
758
- input = begin
759
- self.send("initialize_html_#{library}".to_sym, input, **options)
760
- rescue
761
- raise JSON::LD::JsonLdError::LoadingDocumentFailed, "Malformed HTML document: #{$!.message}"
762
- end
793
+ input = begin
794
+ send("initialize_html_#{library}".to_sym, input, **options)
795
+ rescue StandardError
796
+ raise JSON::LD::JsonLdError::LoadingDocumentFailed, "Malformed HTML document: #{$ERROR_INFO.message}"
797
+ end
763
798
 
764
- # Potentially update options[:base]
765
- if html_base = input.at_xpath("/html/head/base/@href")
766
- base = RDF::URI(url) if url
767
- html_base = RDF::URI(html_base)
768
- html_base = base.join(html_base) if base
769
- yield html_base
799
+ # Potentially update options[:base]
800
+ if (html_base = input.at_xpath("/html/head/base/@href"))
801
+ base = RDF::URI(url) if url
802
+ html_base = RDF::URI(html_base)
803
+ html_base = base.join(html_base) if base
804
+ yield html_base
805
+ end
770
806
  end
771
- end
772
807
 
773
- url = RDF::URI.parse(url)
774
- if url.fragment
775
- id = CGI.unescape(url.fragment)
776
- # Find script with an ID based on that fragment.
777
- element = input.at_xpath("//script[@id='#{id}']")
778
- raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found with id=#{id}" unless element
779
- raise JSON::LD::JsonLdError::LoadingDocumentFailed, "Script tag has type=#{element.attributes['type']}" unless element.attributes['type'].to_s.start_with?('application/ld+json')
780
- content = element.inner_html
781
- validate_input(content, url: url) if options[:validate]
782
- mj_opts = options.keep_if {|k,v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v)}
783
- MultiJson.load(content, **mj_opts)
784
- elsif extractAllScripts
785
- res = []
786
- elements = if profile
787
- es = input.xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]")
788
- # If no profile script, just take a single script without profile
789
- es = [input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")].compact if es.empty?
790
- es
791
- else
792
- input.xpath("//script[starts-with(@type, 'application/ld+json')]")
793
- end
794
- elements.each do |element|
808
+ url = RDF::URI.parse(url)
809
+ if url.fragment
810
+ id = CGI.unescape(url.fragment)
811
+ # Find script with an ID based on that fragment.
812
+ element = input.at_xpath("//script[@id='#{id}']")
813
+ raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found with id=#{id}" unless element
814
+
815
+ unless element.attributes['type'].to_s.start_with?('application/ld+json')
816
+ raise JSON::LD::JsonLdError::LoadingDocumentFailed,
817
+ "Script tag has type=#{element.attributes['type']}"
818
+ end
819
+
795
820
  content = element.inner_html
796
821
  validate_input(content, url: url) if options[:validate]
797
- mj_opts = options.keep_if {|k,v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v)}
798
- r = MultiJson.load(content, **mj_opts)
799
- if r.is_a?(Hash)
800
- res << r
801
- elsif r.is_a?(Array)
802
- res = res.concat(r)
822
+ mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
823
+ MultiJson.load(content, **mj_opts)
824
+ elsif extractAllScripts
825
+ res = []
826
+ elements = if profile
827
+ es = input.xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]")
828
+ # If no profile script, just take a single script without profile
829
+ es = [input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")].compact if es.empty?
830
+ es
831
+ else
832
+ input.xpath("//script[starts-with(@type, 'application/ld+json')]")
833
+ end
834
+ elements.each do |element|
835
+ content = element.inner_html
836
+ validate_input(content, url: url) if options[:validate]
837
+ mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
838
+ r = MultiJson.load(content, **mj_opts)
839
+ if r.is_a?(Hash)
840
+ res << r
841
+ elsif r.is_a?(Array)
842
+ res.concat(r)
843
+ end
803
844
  end
845
+ res
846
+ else
847
+ # Find the first script with type application/ld+json.
848
+ element = input.at_xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]") if profile
849
+ element ||= input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")
850
+ raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found" unless element
851
+
852
+ content = element.inner_html
853
+ validate_input(content, url: url) if options[:validate]
854
+ mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
855
+ MultiJson.load(content, **mj_opts)
804
856
  end
805
- res
806
- else
807
- # Find the first script with type application/ld+json.
808
- element = input.at_xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]") if profile
809
- element ||= input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")
810
- raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found" unless element
811
- content = element.inner_html
812
- validate_input(content, url: url) if options[:validate]
813
- mj_opts = options.keep_if {|k,v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v)}
814
- MultiJson.load(content, **mj_opts)
857
+ rescue MultiJson::ParseError => e
858
+ raise JSON::LD::JsonLdError::InvalidScriptElement, e.message
815
859
  end
816
- rescue MultiJson::ParseError => e
817
- raise JSON::LD::JsonLdError::InvalidScriptElement, e.message
818
- end
819
860
 
820
- ##
821
- # The default serializer for serialzing Ruby Objects to JSON.
822
- #
823
- # Defaults to `MultiJson.dump`
824
- #
825
- # @param [Object] object
826
- # @param [Array<Object>] args
827
- # other arguments that may be passed for some specific implementation.
828
- # @param [Hash<Symbol, Object>] options
829
- # options passed from the invoking context.
830
- # @option options [Object] :serializer_opts (JSON_STATE)
831
- def self.serializer(object, *args, **options)
832
- serializer_opts = options.fetch(:serializer_opts, JSON_STATE)
833
- MultiJson.dump(object, serializer_opts)
834
- end
861
+ ##
862
+ # The default serializer for serialzing Ruby Objects to JSON.
863
+ #
864
+ # Defaults to `MultiJson.dump`
865
+ #
866
+ # @param [Object] object
867
+ # @param [Array<Object>] args
868
+ # other arguments that may be passed for some specific implementation.
869
+ # @param [Hash<Symbol, Object>] options
870
+ # options passed from the invoking context.
871
+ # @option options [Object] :serializer_opts (JSON_STATE)
872
+ def self.serializer(object, *_args, **options)
873
+ serializer_opts = options.fetch(:serializer_opts, JSON_STATE)
874
+ MultiJson.dump(object, serializer_opts)
875
+ end
835
876
 
836
- ##
837
- # Validate JSON using JsonLint, if loaded
838
- private
839
- def self.validate_input(input, url:)
840
- return unless defined?(JsonLint)
841
- jsonlint = JsonLint::Linter.new
842
- input = StringIO.new(input) unless input.respond_to?(:read)
843
- unless jsonlint.check_stream(input)
844
- raise JsonLdError::LoadingDocumentFailed, "url: #{url}\n" + jsonlint.errors[''].join("\n")
877
+ ##
878
+ # Validate JSON using JsonLint, if loaded
879
+
880
+ def self.validate_input(input, url:)
881
+ return unless defined?(JsonLint)
882
+
883
+ jsonlint = JsonLint::Linter.new
884
+ input = StringIO.new(input) unless input.respond_to?(:read)
885
+ unless jsonlint.check_stream(input)
886
+ raise JsonLdError::LoadingDocumentFailed, "url: #{url}\n" + jsonlint.errors[''].join("\n")
887
+ end
888
+
889
+ input.rewind
845
890
  end
846
- input.rewind
847
- end
848
891
 
849
- ##
850
- # A {RemoteDocument} is returned from a {documentLoader}.
851
- class RemoteDocument
852
- # The final URL of the loaded document. This is important to handle HTTP redirects properly.
853
- # @return [String]
854
- attr_accessor :documentUrl
855
-
856
- # The Content-Type of the loaded document, exclusive of any optional parameters.
857
- # @return [String]
858
- attr_reader :contentType
859
-
860
- # @return [String]
861
- # The URL of a remote context as specified by an HTTP Link header with rel=`http://www.w3.org/ns/json-ld#context`
862
- attr_accessor :contextUrl
863
-
864
- # The parsed retrieved document.
865
- # @return [Array<Hash>, Hash]
866
- attr_accessor :document
867
-
868
- # The value of any profile parameter retrieved as part of the original contentType.
869
- # @return [String]
870
- attr_accessor :profile
871
-
872
- # @param [RDF::Util::File::RemoteDocument] document
873
- # @param [String] documentUrl
874
- # The final URL of the loaded document. This is important to handle HTTP redirects properly.
875
- # @param [String] contentType
876
- # The Content-Type of the loaded document, exclusive of any optional parameters.
877
- # @param [String] contextUrl
878
- # The URL of a remote context as specified by an HTTP Link header with rel=`http://www.w3.org/ns/json-ld#context`
879
- # @param [String] profile
880
- # The value of any profile parameter retrieved as part of the original contentType.
881
- # @option options [Hash{Symbol => Object}] options
882
- def initialize(document, documentUrl: nil, contentType: nil, contextUrl: nil, profile: nil, **options)
883
- @document = document
884
- @documentUrl = documentUrl || options[:base_uri]
885
- @contentType = contentType || options[:content_type]
886
- @contextUrl = contextUrl
887
- @profile = profile
892
+ ##
893
+ # A {RemoteDocument} is returned from a {documentLoader}.
894
+ class RemoteDocument
895
+ # The final URL of the loaded document. This is important to handle HTTP redirects properly.
896
+ # @return [String]
897
+ attr_accessor :documentUrl
898
+
899
+ # The Content-Type of the loaded document, exclusive of any optional parameters.
900
+ # @return [String]
901
+ attr_reader :contentType
902
+
903
+ # @return [String]
904
+ # The URL of a remote context as specified by an HTTP Link header with rel=`http://www.w3.org/ns/json-ld#context`
905
+ attr_accessor :contextUrl
906
+
907
+ # The parsed retrieved document.
908
+ # @return [Array<Hash>, Hash]
909
+ attr_accessor :document
910
+
911
+ # The value of any profile parameter retrieved as part of the original contentType.
912
+ # @return [String]
913
+ attr_accessor :profile
914
+
915
+ # @param [RDF::Util::File::RemoteDocument] document
916
+ # @param [String] documentUrl
917
+ # The final URL of the loaded document. This is important to handle HTTP redirects properly.
918
+ # @param [String] contentType
919
+ # The Content-Type of the loaded document, exclusive of any optional parameters.
920
+ # @param [String] contextUrl
921
+ # The URL of a remote context as specified by an HTTP Link header with rel=`http://www.w3.org/ns/json-ld#context`
922
+ # @param [String] profile
923
+ # The value of any profile parameter retrieved as part of the original contentType.
924
+ # @option options [Hash{Symbol => Object}] options
925
+ def initialize(document, documentUrl: nil, contentType: nil, contextUrl: nil, profile: nil, **options)
926
+ @document = document
927
+ @documentUrl = documentUrl || options[:base_uri]
928
+ @contentType = contentType || options[:content_type]
929
+ @contextUrl = contextUrl
930
+ @profile = profile
931
+ end
888
932
  end
889
933
  end
890
934
  end
891
935
  end
892
-