json-ld 3.1.2 → 3.1.7

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.
@@ -46,7 +46,7 @@ module JSON::LD
46
46
  #
47
47
  # @param [Hash{String => String}] env
48
48
  # @return [Array(Integer, Hash, #each)] Status, Headers and Body
49
- # @see http://rack.rubyforge.org/doc/SPEC.html
49
+ # @see https://rubydoc.info/github/rack/rack/file/SPEC
50
50
  def call(env)
51
51
  response = app.call(env)
52
52
  body = response[2].respond_to?(:body) ? response[2].body : response[2]
@@ -3,6 +3,8 @@
3
3
  require 'json'
4
4
  require 'bigdecimal'
5
5
  require 'set'
6
+ require 'rdf/util/cache'
7
+
6
8
  begin
7
9
  # Attempt to load this to avoid unnecessary context fetches
8
10
  require 'json-ld-preloaded'
@@ -21,6 +23,14 @@ module JSON::LD
21
23
  # @return [Hash{Symbol => Context}]
22
24
  PRELOADED = {}
23
25
 
26
+ # Initial contexts, defined on first access
27
+ INITIAL_CONTEXTS = {}
28
+
29
+ ##
30
+ # Defines the maximum number of interned URI references that can be held
31
+ # cached in memory at any one time.
32
+ CACHE_SIZE = 100 # unlimited by default
33
+
24
34
  class << self
25
35
  ##
26
36
  # Add preloaded context. In the block form, the context is lazy evaulated on first use.
@@ -40,242 +50,11 @@ module JSON::LD
40
50
  end
41
51
  end
42
52
 
43
- # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
44
- class TermDefinition
45
- # @return [RDF::URI] IRI map
46
- attr_accessor :id
47
-
48
- # @return [String] term name
49
- attr_accessor :term
50
-
51
- # @return [String] Type mapping
52
- attr_accessor :type_mapping
53
-
54
- # Base container mapping, without @set
55
- # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
56
- attr_reader :container_mapping
57
-
58
- # @return [String] Term used for nest properties
59
- attr_accessor :nest
60
-
61
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
62
- # @return [String] Language mapping
63
- attr_accessor :language_mapping
64
-
65
- # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
66
- # @return ["ltr", "rtl"] direction_mapping
67
- attr_accessor :direction_mapping
68
-
69
- # @return [Boolean] Reverse Property
70
- attr_accessor :reverse_property
71
-
72
- # This is a simple term definition, not an expanded term definition
73
- # @return [Boolean]
74
- attr_accessor :simple
75
-
76
- # Property used for data indexing; defaults to @index
77
- # @return [Boolean]
78
- attr_accessor :index
79
-
80
- # Indicate that term may be used as a prefix
81
- attr_writer :prefix
82
-
83
- # Term-specific context
84
- # @return [Hash{String => Object}]
85
- attr_accessor :context
86
-
87
- # Term is protected.
88
- # @return [Boolean]
89
- attr_writer :protected
90
-
91
- # This is a simple term definition, not an expanded term definition
92
- # @return [Boolean] simple
93
- def simple?; simple; end
94
-
95
- # This is an appropriate term to use as the prefix of a compact IRI
96
- # @return [Boolean] simple
97
- def prefix?; @prefix; end
98
-
99
- # Create a new Term Mapping with an ID
100
- # @param [String] term
101
- # @param [String] id
102
- # @param [String] type_mapping Type mapping
103
- # @param [Set<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
104
- # @param [String] language_mapping
105
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term
106
- # @param ["ltr", "rtl"] direction_mapping
107
- # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
108
- # @param [Boolean] reverse_property
109
- # @param [Boolean] protected
110
- # @param [String] nest term used for nest properties
111
- # @param [Boolean] simple
112
- # This is a simple term definition, not an expanded term definition
113
- # @param [Boolean] prefix
114
- # Term may be used as a prefix
115
- def initialize(term,
116
- id: nil,
117
- index: nil,
118
- type_mapping: nil,
119
- container_mapping: nil,
120
- language_mapping: nil,
121
- direction_mapping: nil,
122
- reverse_property: false,
123
- nest: nil,
124
- protected: false,
125
- simple: false,
126
- prefix: nil,
127
- context: nil)
128
- @term = term
129
- @id = id.to_s unless id.nil?
130
- @index = index.to_s unless index.nil?
131
- @type_mapping = type_mapping.to_s unless type_mapping.nil?
132
- self.container_mapping = container_mapping
133
- @language_mapping = language_mapping unless language_mapping.nil?
134
- @direction_mapping = direction_mapping unless direction_mapping.nil?
135
- @reverse_property = reverse_property
136
- @protected = protected
137
- @nest = nest unless nest.nil?
138
- @simple = simple
139
- @prefix = prefix unless prefix.nil?
140
- @context = context unless context.nil?
141
- end
142
-
143
- # Term is protected.
144
- # @return [Boolean]
145
- def protected?; !!@protected; end
146
-
147
- # Set container mapping, from an array which may include @set
148
- def container_mapping=(mapping)
149
- mapping = case mapping
150
- when Set then mapping
151
- when Array then Set.new(mapping)
152
- when String then Set[mapping]
153
- when nil then Set.new
154
- else
155
- raise "Shouldn't happen with #{mapping.inspect}"
156
- end
157
- if @as_set = mapping.include?('@set')
158
- mapping = mapping.dup
159
- mapping.delete('@set')
160
- end
161
- @container_mapping = mapping
162
- @index ||= '@index' if mapping.include?('@index')
163
- end
164
-
165
- ##
166
- # Output Hash or String definition for this definition considering @language and @vocab
167
- #
168
- # @param [Context] context
169
- # @return [String, Hash{String => Array[String], String}]
170
- def to_context_definition(context)
171
- cid = if context.vocab && id.start_with?(context.vocab)
172
- # Nothing to return unless it's the same as the vocab
173
- id == context.vocab ? context.vocab : id.to_s[context.vocab.length..-1]
174
- else
175
- # Find a term to act as a prefix
176
- iri, prefix = context.iri_to_term.detect {|i,p| id.to_s.start_with?(i.to_s)}
177
- iri && iri != id ? "#{prefix}:#{id.to_s[iri.length..-1]}" : id
178
- end
179
-
180
- if simple?
181
- cid.to_s unless cid == term && context.vocab
182
- else
183
- defn = {}
184
- defn[reverse_property ? '@reverse' : '@id'] = cid.to_s unless cid == term && !reverse_property
185
- if type_mapping
186
- defn['@type'] = if KEYWORDS.include?(type_mapping)
187
- type_mapping
188
- else
189
- context.compact_iri(type_mapping, vocab: true)
190
- end
191
- end
192
-
193
- cm = Array(container_mapping)
194
- cm << "@set" if as_set? && !cm.include?("@set")
195
- cm = cm.first if cm.length == 1
196
- defn['@container'] = cm unless cm.empty?
197
- # Language set as false to be output as null
198
- defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
199
- defn['@context'] = @context if @context
200
- defn['@nest'] = @nest if @nest
201
- defn['@index'] = @index if @index
202
- defn['@prefix'] = @prefix unless @prefix.nil?
203
- defn
204
- end
205
- end
206
-
207
- ##
208
- # Turn this into a source for a new instantiation
209
- # FIXME: context serialization
210
- # @return [String]
211
- def to_rb
212
- defn = [%(TermDefinition.new\(#{term.inspect})]
213
- %w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
214
- v = instance_variable_get("@#{acc}".to_sym)
215
- v = v.to_s if v.is_a?(RDF::Term)
216
- if acc == 'container_mapping'
217
- v = v.to_a
218
- v << '@set' if as_set?
219
- v = v.first if v.length <= 1
220
- end
221
- defn << "#{acc}: #{v.inspect}" if v
222
- end
223
- defn.join(', ') + ")"
224
- end
225
-
226
- # If container mapping was defined along with @set
227
- # @return [Boolean]
228
- def as_set?; @as_set || false; end
229
-
230
- # Check if term definitions are identical, modulo @protected
231
- # @return [Boolean]
232
- def ==(other)
233
- other.is_a?(TermDefinition) &&
234
- id == other.id &&
235
- term == other.term &&
236
- type_mapping == other.type_mapping &&
237
- container_mapping == other.container_mapping &&
238
- nest == other.nest &&
239
- language_mapping == other.language_mapping &&
240
- direction_mapping == other.direction_mapping &&
241
- reverse_property == other.reverse_property &&
242
- simple == other.simple &&
243
- index == other.index &&
244
- context == other.context &&
245
- prefix? == other.prefix? &&
246
- as_set? == other.as_set?
247
- end
248
-
249
- def inspect
250
- v = %w([TD)
251
- v << "id=#{@id}"
252
- v << "index=#{index.inspect}" unless index.nil?
253
- v << "term=#{@term}"
254
- v << "rev" if reverse_property
255
- v << "container=#{container_mapping}" if container_mapping
256
- v << "as_set=#{as_set?.inspect}"
257
- v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
258
- v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
259
- v << "type=#{type_mapping}" unless type_mapping.nil?
260
- v << "nest=#{nest.inspect}" unless nest.nil?
261
- v << "simple=true" if @simple
262
- v << "protected=true" if @protected
263
- v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
264
- v << "has-context" unless context.nil?
265
- v.join(" ") + "]"
266
- end
267
- end
268
-
269
53
  # The base.
270
54
  #
271
55
  # @return [RDF::URI] Current base IRI, used for expanding relative IRIs.
272
56
  attr_reader :base
273
57
 
274
- # The base.
275
- #
276
- # @return [RDF::URI] Document base IRI, to initialize `base`.
277
- attr_reader :doc_base
278
-
279
58
  # @return [RDF::URI] base IRI of the context, if loaded remotely.
280
59
  attr_accessor :context_base
281
60
 
@@ -316,9 +95,6 @@ module JSON::LD
316
95
  # @return [Hash{Symbol => Object}] Global options used in generating IRIs
317
96
  attr_accessor :options
318
97
 
319
- # @return [Context] A context provided to us that we can use without re-serializing XXX
320
- attr_accessor :provided_context
321
-
322
98
  # @return [BlankNodeNamer]
323
99
  attr_accessor :namer
324
100
 
@@ -328,187 +104,117 @@ module JSON::LD
328
104
  # @see #initialize
329
105
  # @see #parse
330
106
  # @param [String, #read, Array, Hash, Context] local_context
331
- # @raise [JsonLdError]
332
- # on a remote context load error, syntax error, or a reference to a term which is not defined.
333
- # @return [Context]
334
- def self.parse(local_context, protected: false, override_protected: false, propagate: true, **options)
335
- self.new(**options).parse(local_context, protected: false, override_protected: override_protected, propagate: propagate)
336
- end
337
-
338
- ##
339
- # Create new evaluation context
340
- # @param [Hash] options
341
- # @option options [String, #to_s] :base
107
+ # @param [String, #to_s] base (nil)
342
108
  # 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.
343
- # @option options [Proc] :documentLoader
344
- # 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 {API.documentLoader} for the method signature.
345
- # @option options [Hash{Symbol => String}] :prefixes
346
- # See `RDF::Reader#initialize`
347
- # @option options [String, #to_s] :vocab
348
- # Initial value for @vocab
349
- # @option options [String, #to_s] :language
350
- # Initial value for @langauge
351
- # @yield [ec]
352
- # @yieldparam [Context]
353
- # @return [Context]
354
- def initialize(**options)
355
- if options[:base]
356
- @base = @doc_base = RDF::URI(options[:base]).dup
357
- @doc_base.canonicalize! if options[:canonicalize]
358
- end
359
- self.processingMode = options[:processingMode] if options.has_key?(:processingMode)
360
- @term_definitions = {}
361
- @iri_to_term = {
362
- RDF.to_uri.to_s => "rdf",
363
- RDF::XSD.to_uri.to_s => "xsd"
364
- }
365
- @namer = BlankNodeMapper.new("t")
366
-
367
- @options = options
368
-
369
- # Load any defined prefixes
370
- (options[:prefixes] || {}).each_pair do |k, v|
371
- next if k.nil?
372
- @iri_to_term[v.to_s] = k
373
- @term_definitions[k.to_s] = TermDefinition.new(k, id: v.to_s, simple: true, prefix: true)
374
- end
375
-
376
- self.vocab = options[:vocab] if options[:vocab]
377
- self.default_language = options[:language] if options[:language] =~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
378
- @term_definitions = options[:term_definitions] if options[:term_definitions]
379
-
380
- #log_debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
381
-
382
- yield(self) if block_given?
383
- end
384
-
385
- ##
386
- # Initial context, without mappings, vocab or default language
387
- #
388
- # @return [Boolean]
389
- def empty?
390
- @term_definitions.empty? && self.vocab.nil? && self.default_language.nil?
391
- end
392
-
393
- # @param [String] value must be an absolute IRI
394
- def base=(value, **options)
395
- if value
396
- raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
397
- value = RDF::URI(value).dup
398
- value = @base.join(value) if @base && value.relative?
399
- @base = value
400
- @base.canonicalize! if @options[:canonicalize]
401
- raise JsonLdError::InvalidBaseIRI, "@base must be an absolute IRI: #{value.inspect}" unless @base.absolute? || !@options[:validate]
402
- @base
403
- else
404
- @base = nil
405
- end
406
-
407
- end
408
-
409
- # @param [String] value
410
- def default_language=(value, **options)
411
- @default_language = case value
412
- when String
413
- # Warn on an invalid language tag, unless :validate is true, in which case it's an error
414
- if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
415
- warn "@language must be valid BCP47: #{value.inspect}"
416
- end
417
- options[:lowercaseLanguage] ? value.downcase : value
418
- when nil
419
- nil
109
+ # @param [Boolean] override_protected (false)
110
+ # Protected terms may be cleared.
111
+ # @param [Boolean] propagate (true)
112
+ # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
113
+ # @raise [JsonLdError]
114
+ # on a remote context load error, syntax error, or a reference to a term which is not defined.
115
+ # @return [Context]
116
+ def self.parse(local_context,
117
+ base: nil,
118
+ override_protected: false,
119
+ propagate: true,
120
+ **options)
121
+ c = self.new(**options)
122
+ if local_context.respond_to?(:empty?) && local_context.empty?
123
+ c
420
124
  else
421
- raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
125
+ c.parse(local_context,
126
+ base: base,
127
+ override_protected: override_protected,
128
+ propagate: propagate)
422
129
  end
423
130
  end
424
131
 
425
- # @param [String] value
426
- def default_direction=(value, **options)
427
- @default_direction = if value
428
- raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
429
- value
430
- else
431
- nil
432
- end
132
+ ##
133
+ # Class-level cache used for retaining parsed remote contexts.
134
+ #
135
+ # @return [RDF::Util::Cache]
136
+ # @private
137
+ def self.cache
138
+ @cache ||= RDF::Util::Cache.new(CACHE_SIZE)
433
139
  end
434
140
 
435
141
  ##
436
- # Retrieve, or check processing mode.
142
+ # Class-level cache inverse contexts.
437
143
  #
438
- # * With no arguments, retrieves the current set processingMode.
439
- # * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
440
- # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
441
- #
442
- # @param [String, Number] expected (nil)
443
- # @return [String]
444
- def processingMode(expected = nil)
445
- case expected
446
- when 1.0, 'json-ld-1.0'
447
- @processingMode == 'json-ld-1.0'
448
- when 1.1, 'json-ld-1.1'
449
- @processingMode ||= 'json-ld-1.1'
450
- @processingMode == 'json-ld-1.1'
451
- when nil
452
- @processingMode
453
- else
454
- false
455
- end
144
+ # @return [RDF::Util::Cache]
145
+ # @private
146
+ def self.inverse_cache
147
+ @inverse_cache ||= RDF::Util::Cache.new(CACHE_SIZE)
456
148
  end
457
149
 
458
150
  ##
459
- # Set processing mode.
460
- #
461
- # * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
462
- #
463
- # If contex has a @version member, it's value MUST be 1.1, otherwise an "invalid @version value" has been detected, and processing is aborted.
464
- # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
465
- #
466
- # @param [String, Number] expected
467
- # @return [String]
468
- # @raise [JsonLdError::ProcessingModeConflict]
469
- def processingMode=(value = nil, **options)
470
- value = "json-ld-1.1" if value == 1.1
471
- case value
472
- when "json-ld-1.0", "json-ld-1.1"
473
- if @processingMode && @processingMode != value
474
- raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
151
+ # @private
152
+ # Allow caching of well-known contexts
153
+ def self.new(**options)
154
+ if (options.keys - [
155
+ :compactArrays,
156
+ :documentLoader,
157
+ :extractAllScripts,
158
+ :ordered,
159
+ :processingMode,
160
+ :validate
161
+ ]).empty?
162
+ # allow caching
163
+ key = options.hash
164
+ INITIAL_CONTEXTS[key] ||= begin
165
+ context = JSON::LD::Context.allocate
166
+ context.send(:initialize, **options)
167
+ context.freeze
168
+ context.term_definitions.freeze
169
+ context
475
170
  end
476
- @processingMode = value
477
171
  else
478
- raise JsonLdError::InvalidVersionValue, value.inspect
172
+ # Don't try to cache
173
+ context = JSON::LD::Context.allocate
174
+ context.send(:initialize, **options)
175
+ context
479
176
  end
480
177
  end
481
178
 
482
- # If context has a @vocab member: if its value is not a valid absolute IRI or null trigger an INVALID_VOCAB_MAPPING error; otherwise set the active context's vocabulary mapping to its value and remove the @vocab member from context.
483
- # @param [String] value must be an absolute IRI
484
- def vocab=(value, **options)
485
- @vocab = case value
486
- when /_:/
487
- # BNode vocab is deprecated
488
- warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
489
- value
490
- when String, RDF::URI
491
- if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
492
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
493
- end
494
- v = expand_iri(value.to_s, vocab: true, documentRelative: true)
495
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}" if !v.valid? && @options[:validate]
496
- v
497
- when nil
498
- nil
499
- else
500
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
179
+ ##
180
+ # Create new evaluation context
181
+ # @param [Hash] options
182
+ # @option options [Hash{Symbol => String}] :prefixes
183
+ # See `RDF::Reader#initialize`
184
+ # @option options [String, #to_s] :vocab
185
+ # Initial value for @vocab
186
+ # @option options [String, #to_s] :language
187
+ # Initial value for @langauge
188
+ # @yield [ec]
189
+ # @yieldparam [Context]
190
+ # @return [Context]
191
+ def initialize(**options)
192
+ if options[:processingMode] == 'json-ld-1.0'
193
+ @processingMode = 'json-ld-1.0'
501
194
  end
502
- end
195
+ @term_definitions = {}
196
+ @iri_to_term = {
197
+ RDF.to_uri.to_s => "rdf",
198
+ RDF::XSD.to_uri.to_s => "xsd"
199
+ }
200
+ @namer = BlankNodeMapper.new("t")
503
201
 
504
- # Set propagation
505
- # @note: by the time this is called, the work has already been done.
506
- #
507
- # @param [Boolean] value
508
- def propagate=(value, **options)
509
- raise JsonLdError::InvalidContextEntry, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
510
- raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
511
- value
202
+ @options = options
203
+
204
+ # Load any defined prefixes
205
+ (options[:prefixes] || {}).each_pair do |k, v|
206
+ next if k.nil?
207
+ @iri_to_term[v.to_s] = k
208
+ @term_definitions[k.to_s] = TermDefinition.new(k, id: v.to_s, simple: true, prefix: true)
209
+ end
210
+
211
+ self.vocab = options[:vocab] if options[:vocab]
212
+ self.default_language = options[:language] if options[:language] =~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
213
+ @term_definitions = options[:term_definitions] if options[:term_definitions]
214
+
215
+ #log_debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
216
+
217
+ yield(self) if block_given?
512
218
  end
513
219
 
514
220
  # Create an Evaluation Context
@@ -521,11 +227,12 @@ module JSON::LD
521
227
  #
522
228
  #
523
229
  # @param [String, #read, Array, Hash, Context] local_context
524
- # @param [Array<String>] remote_contexts
525
- # @param [Boolean] protected Make defined terms protected (as if `@protected` were used).
230
+ # @param [String, #to_s] base
231
+ # 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.
526
232
  # @param [Boolean] override_protected Protected terms may be cleared.
527
- # @param [Boolean] propagate
233
+ # @param [Boolean] propagate (true)
528
234
  # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
235
+ # @param [Array<String>] remote_contexts ([])
529
236
  # @param [Boolean] validate_scoped (true).
530
237
  # Validate scoped context, loading if necessary.
531
238
  # If false, do not load scoped contexts.
@@ -534,13 +241,12 @@ module JSON::LD
534
241
  # @return [Context]
535
242
  # @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
536
243
  def parse(local_context,
537
- remote_contexts: [],
538
- protected: false,
244
+ base: nil,
539
245
  override_protected: false,
540
246
  propagate: true,
247
+ remote_contexts: [],
541
248
  validate_scoped: true)
542
249
  result = self.dup
543
- result.provided_context = local_context if self.empty?
544
250
  # Early check for @propagate, which can only appear in a local context
545
251
  propagate = local_context.is_a?(Hash) ? local_context.fetch('@propagate', propagate) : propagate
546
252
  result.previous_context ||= result.dup unless propagate
@@ -549,7 +255,7 @@ module JSON::LD
549
255
 
550
256
  local_context.each do |context|
551
257
  case context
552
- when nil
258
+ when nil,false
553
259
  # 3.1 If the `override_protected` is false, and the active context contains protected terms, an error is raised.
554
260
  if override_protected || result.term_definitions.values.none?(&:protected?)
555
261
  null_context = Context.new(**options)
@@ -561,28 +267,26 @@ module JSON::LD
561
267
  end
562
268
  when Context
563
269
  #log_debug("parse") {"context: #{context.inspect}"}
564
- result = context.dup
270
+ result = result.merge(context)
565
271
  when IO, StringIO
566
272
  #log_debug("parse") {"io: #{context}"}
567
273
  # Load context document, if it is an open file
568
274
  begin
569
275
  ctx = JSON.load(context)
570
276
  raise JSON::LD::JsonLdError::InvalidRemoteContext, "Context missing @context key" if @options[:validate] && ctx['@context'].nil?
571
- result = result.dup.parse(ctx["@context"] ? ctx["@context"].dup : {})
572
- result.provided_context = ctx["@context"] if [context] == local_context
573
- result
277
+ result = result.parse(ctx["@context"] ? ctx["@context"] : {})
574
278
  rescue JSON::ParserError => e
575
279
  #log_debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
576
280
  raise JSON::LD::JsonLdError::InvalidRemoteContext, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
577
- self.dup
281
+ self
578
282
  end
579
283
  when String, RDF::URI
580
284
  #log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
581
285
 
582
286
  # 3.2.1) Set context to the result of resolving value against the base IRI which is established as specified in section 5.1 Establishing a Base URI of [RFC3986]. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
583
- context = RDF::URI(result.context_base || options[:base]).join(context)
287
+ context = RDF::URI(result.context_base || base).join(context)
584
288
  context_canon = context.canonicalize
585
- context_canon.scheme == 'http' if context_canon.scheme == 'https'
289
+ context_canon.scheme = 'http' if context_canon.scheme == 'https'
586
290
 
587
291
  # If validating a scoped context which has already been loaded, skip to the next one
588
292
  next if !validate_scoped && remote_contexts.include?(context.to_s)
@@ -590,11 +294,7 @@ module JSON::LD
590
294
  remote_contexts << context.to_s
591
295
  raise JsonLdError::ContextOverflow, "#{context}" if remote_contexts.length >= MAX_CONTEXTS_LOADED
592
296
 
593
- context_no_base = result.dup
594
- context_no_base.base = nil
595
- context_no_base.context_base = context.to_s
596
-
597
- if PRELOADED[context_canon.to_s]
297
+ cached_context = if PRELOADED[context_canon.to_s]
598
298
  # If we have a cached context, merge it into the current context (result) and use as the new context
599
299
  #log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"}
600
300
 
@@ -603,10 +303,10 @@ module JSON::LD
603
303
  #log_debug("parse") {"=> (call)"}
604
304
  PRELOADED[context_canon.to_s] = PRELOADED[context_canon.to_s].call
605
305
  end
606
- context = context_no_base.merge!(PRELOADED[context_canon.to_s])
306
+ PRELOADED[context_canon.to_s]
607
307
  else
608
308
  # Load context document, if it is a string
609
- begin
309
+ Context.cache[context_canon.to_s] ||= begin
610
310
  context_opts = @options.merge(
611
311
  profile: 'http://www.w3.org/ns/json-ld#context',
612
312
  requestProfile: 'http://www.w3.org/ns/json-ld#context',
@@ -615,31 +315,30 @@ module JSON::LD
615
315
  JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
616
316
  # 3.2.5) Dereference context. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member.
617
317
  raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
618
- context = remote_doc.document['@context']
318
+
319
+ # Parse stand-alone
320
+ ctx = Context.new(unfrozen: true, **options).dup
321
+ ctx.context_base = context.to_s
322
+ ctx = ctx.parse(remote_doc.document['@context'], remote_contexts: remote_contexts.dup)
323
+ ctx.instance_variable_set(:@base, nil)
324
+ ctx
619
325
  end
620
326
  rescue JsonLdError::LoadingDocumentFailed => e
621
327
  #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
622
- raise JsonLdError::LoadingRemoteContextFailed, "#{context_no_base.context_base}: #{e.message}", e.backtrace
328
+ raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
623
329
  rescue JsonLdError
624
330
  raise
625
331
  rescue StandardError => e
626
332
  #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
627
- raise JsonLdError::LoadingRemoteContextFailed, "#{context_no_base.context_base}: #{e.message}", e.backtrace
333
+ raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
628
334
  end
629
-
630
- # 3.2.6) Set context to the result of recursively calling this algorithm, passing context no base for active context, context for local context, and remote contexts.
631
- context = context_no_base.parse(context,
632
- remote_contexts: remote_contexts.dup,
633
- protected: protected,
634
- override_protected: override_protected,
635
- propagate: propagate,
636
- validate_scoped: validate_scoped)
637
- PRELOADED[context_canon.to_s] = context.dup
638
- context.provided_context = result.provided_context
639
335
  end
640
- context.base ||= result.base
336
+
337
+ # Merge loaded context noting protected term overriding
338
+ context = result.merge(cached_context, override_protected: override_protected)
339
+
340
+ context.previous_context = self unless propagate
641
341
  result = context
642
- #log_debug("parse") {"=> provided_context: #{context.inspect}"}
643
342
  when Hash
644
343
  context = context.dup # keep from modifying a hash passed as a param
645
344
 
@@ -658,31 +357,33 @@ module JSON::LD
658
357
  # Retrieve remote context and merge the remaining context object into the result.
659
358
  raise JsonLdError::InvalidContextEntry, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0")
660
359
  raise JsonLdError::InvalidImportValue, "@import must be a string: #{context['@import'].inspect}" unless context['@import'].is_a?(String)
661
- source = RDF::URI(result.context_base || result.base).join(context['@import'])
360
+ import_loc = RDF::URI(result.context_base || base).join(context['@import'])
662
361
  begin
663
362
  context_opts = @options.merge(
664
363
  profile: 'http://www.w3.org/ns/json-ld#context',
665
364
  requestProfile: 'http://www.w3.org/ns/json-ld#context',
666
365
  base: nil)
667
366
  context_opts.delete(:headers)
668
- JSON::LD::API.loadRemoteDocument(source, **context_opts) do |remote_doc|
669
- # Dereference source. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member.
670
- raise JsonLdError::InvalidRemoteContext, "#{source}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
367
+ # FIXME: should cache this, but ContextCache is for parsed contexts
368
+ JSON::LD::API.loadRemoteDocument(import_loc, **context_opts) do |remote_doc|
369
+ # Dereference import_loc. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member.
370
+ raise JsonLdError::InvalidRemoteContext, "#{import_loc}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
671
371
  import_context = remote_doc.document['@context']
372
+ import_context.delete('@base')
672
373
  raise JsonLdError::InvalidRemoteContext, "#{import_context.to_json} must be an object" unless import_context.is_a?(Hash)
673
374
  raise JsonLdError::InvalidContextEntry, "#{import_context.to_json} must not include @import entry" if import_context.has_key?('@import')
674
375
  context.delete(key)
675
376
  context = import_context.merge(context)
676
377
  end
677
378
  rescue JsonLdError::LoadingDocumentFailed => e
678
- raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
379
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
679
380
  rescue JsonLdError
680
381
  raise
681
382
  rescue StandardError => e
682
- raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
383
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
683
384
  end
684
385
  else
685
- result.send(setter, context[key], remote_contexts: remote_contexts, protected: context.fetch('@protected', protected))
386
+ result.send(setter, context[key], remote_contexts: remote_contexts)
686
387
  end
687
388
  context.delete(key)
688
389
  end
@@ -693,8 +394,9 @@ module JSON::LD
693
394
  context.each_key do |key|
694
395
  # ... where key is not @base, @vocab, @language, or @version
695
396
  result.create_term_definition(context, key, defined,
397
+ base: base,
696
398
  override_protected: override_protected,
697
- protected: context.fetch('@protected', protected),
399
+ protected: context['@protected'],
698
400
  remote_contexts: remote_contexts.dup,
699
401
  validate_scoped: validate_scoped
700
402
  ) unless NON_TERMDEF_KEYS.include?(key)
@@ -711,28 +413,29 @@ module JSON::LD
711
413
  # Merge in a context, creating a new context with updates from `context`
712
414
  #
713
415
  # @param [Context] context
416
+ # @param [Boolean] override_protected Allow or disallow protected terms to be changed
714
417
  # @return [Context]
715
- def merge(context)
716
- c = self.dup.merge!(context)
717
- c.instance_variable_set(:@term_definitions, context.term_definitions.dup)
718
- c
719
- end
418
+ def merge(context, override_protected: false)
419
+ ctx = Context.new(term_definitions: self.term_definitions, standard_prefixes: options[:standard_prefixes])
420
+ ctx.context_base = context.context_base || self.context_base
421
+ ctx.default_language = context.default_language || self.default_language
422
+ ctx.default_direction = context.default_direction || self.default_direction
423
+ ctx.vocab = context.vocab || self.vocab
424
+ ctx.base = self.base unless self.base.nil?
425
+ if !override_protected
426
+ ctx.term_definitions.each do |term, definition|
427
+ next unless definition.protected? && (other = context.term_definitions[term])
428
+ unless definition == other
429
+ raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
430
+ end
431
+ end
432
+ end
720
433
 
721
- ##
722
- # Update context with definitions from `context`
723
- #
724
- # @param [Context] context
725
- # @return [self]
726
- def merge!(context)
727
- # FIXME: if new context removes the default language, this won't do anything
728
- self.default_language = context.default_language if context.default_language
729
- self.vocab = context.vocab if context.vocab
730
- self.base = context.base if context.base
731
-
732
- # Merge in Term Definitions
733
- term_definitions.merge!(context.term_definitions)
734
- @inverse_context = nil # Re-build after term definitions set
735
- self
434
+ # Add term definitions
435
+ context.term_definitions.each do |term, definition|
436
+ ctx.term_definitions[term] = definition
437
+ end
438
+ ctx
736
439
  end
737
440
 
738
441
  # The following constants are used to reduce object allocations in #create_term_definition below
@@ -755,10 +458,9 @@ module JSON::LD
755
458
  # @param [Hash] local_context
756
459
  # @param [String] term
757
460
  # @param [Hash] defined
461
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
758
462
  # @param [Boolean] protected if true, causes all terms to be marked protected
759
463
  # @param [Boolean] override_protected Protected terms may be cleared.
760
- # @param [Boolean] propagate
761
- # Context is propagated across node objects.
762
464
  # @param [Array<String>] remote_contexts
763
465
  # @param [Boolean] validate_scoped (true).
764
466
  # Validate scoped context, loading if necessary.
@@ -767,8 +469,9 @@ module JSON::LD
767
469
  # Represents a cyclical term dependency
768
470
  # @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
769
471
  def create_term_definition(local_context, term, defined,
472
+ base: nil,
770
473
  override_protected: false,
771
- protected: false,
474
+ protected: nil,
772
475
  remote_contexts: [],
773
476
  validate_scoped: true)
774
477
  # Expand a string value, unless it matches a keyword
@@ -985,9 +688,17 @@ module JSON::LD
985
688
 
986
689
  if value.has_key?('@context')
987
690
  begin
988
- self.parse(value['@context'], override_protected: true, remote_contexts: remote_contexts, validate_scoped: false)
691
+ new_ctx = self.parse(value['@context'],
692
+ base: base,
693
+ override_protected: true,
694
+ remote_contexts: remote_contexts,
695
+ validate_scoped: false)
989
696
  # Record null context in array form
990
- definition.context = value['@context'] ? value['@context'] : [nil]
697
+ definition.context = case value['@context']
698
+ when String then new_ctx.context_base
699
+ when nil then [nil]
700
+ else value['@context']
701
+ end
991
702
  rescue JsonLdError => e
992
703
  raise JsonLdError::InvalidScopedContext, "Term definition for #{term.inspect} contains illegal value for @context: #{e.message}"
993
704
  end
@@ -1045,9 +756,130 @@ module JSON::LD
1045
756
 
1046
757
  term_definitions[term] = definition
1047
758
  defined[term] = true
1048
- ensure
1049
- # Re-build after term definitions set
1050
- @inverse_context = nil
759
+ end
760
+
761
+ ##
762
+ # Initial context, without mappings, vocab or default language
763
+ #
764
+ # @return [Boolean]
765
+ def empty?
766
+ @term_definitions.empty? && self.vocab.nil? && self.default_language.nil?
767
+ end
768
+
769
+ # @param [String] value must be an absolute IRI
770
+ def base=(value, **options)
771
+ if value
772
+ raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
773
+ value = RDF::URI(value)
774
+ value = @base.join(value) if @base && value.relative?
775
+ # still might be relative to document
776
+ @base = value
777
+ else
778
+ @base = false
779
+ end
780
+
781
+ end
782
+
783
+ # @param [String] value
784
+ def default_language=(value, **options)
785
+ @default_language = case value
786
+ when String
787
+ # Warn on an invalid language tag, unless :validate is true, in which case it's an error
788
+ if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
789
+ warn "@language must be valid BCP47: #{value.inspect}"
790
+ end
791
+ options[:lowercaseLanguage] ? value.downcase : value
792
+ when nil
793
+ nil
794
+ else
795
+ raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
796
+ end
797
+ end
798
+
799
+ # @param [String] value
800
+ def default_direction=(value, **options)
801
+ @default_direction = if value
802
+ raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
803
+ value
804
+ else
805
+ nil
806
+ end
807
+ end
808
+
809
+ ##
810
+ # Retrieve, or check processing mode.
811
+ #
812
+ # * With no arguments, retrieves the current set processingMode.
813
+ # * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
814
+ # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
815
+ #
816
+ # @param [String, Number] expected (nil)
817
+ # @return [String]
818
+ def processingMode(expected = nil)
819
+ case expected
820
+ when 1.0, 'json-ld-1.0'
821
+ @processingMode == 'json-ld-1.0'
822
+ when 1.1, 'json-ld-1.1'
823
+ @processingMode.nil? || @processingMode == 'json-ld-1.1'
824
+ when nil
825
+ @processingMode || 'json-ld-1.1'
826
+ else
827
+ false
828
+ end
829
+ end
830
+
831
+ ##
832
+ # Set processing mode.
833
+ #
834
+ # * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
835
+ #
836
+ # If contex has a @version member, it's value MUST be 1.1, otherwise an "invalid @version value" has been detected, and processing is aborted.
837
+ # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
838
+ #
839
+ # @param [String, Number] value
840
+ # @return [String]
841
+ # @raise [JsonLdError::ProcessingModeConflict]
842
+ def processingMode=(value = nil, **options)
843
+ value = "json-ld-1.1" if value == 1.1
844
+ case value
845
+ when "json-ld-1.0", "json-ld-1.1"
846
+ if @processingMode && @processingMode != value
847
+ raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
848
+ end
849
+ @processingMode = value
850
+ else
851
+ raise JsonLdError::InvalidVersionValue, value.inspect
852
+ end
853
+ end
854
+
855
+ # If context has a @vocab member: if its value is not a valid absolute IRI or null trigger an INVALID_VOCAB_MAPPING error; otherwise set the active context's vocabulary mapping to its value and remove the @vocab member from context.
856
+ # @param [String] value must be an absolute IRI
857
+ def vocab=(value, **options)
858
+ @vocab = case value
859
+ when /_:/
860
+ # BNode vocab is deprecated
861
+ warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
862
+ value
863
+ when String, RDF::URI
864
+ if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
865
+ raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
866
+ end
867
+ expand_iri(value.to_s, vocab: true, documentRelative: true)
868
+ when nil
869
+ nil
870
+ else
871
+ raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
872
+ end
873
+ end
874
+
875
+ # Set propagation
876
+ # @note: by the time this is called, the work has already been done.
877
+ #
878
+ # @param [Boolean] value
879
+ def propagate=(value, **options)
880
+ raise JsonLdError::InvalidContextEntry, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
881
+ raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
882
+ value
1051
883
  end
1052
884
 
1053
885
  ##
@@ -1056,40 +888,44 @@ module JSON::LD
1056
888
  # If a context was supplied in global options, use that, otherwise, generate one
1057
889
  # from this representation.
1058
890
  #
891
+ # @param [Array, Hash, Context, IO, StringIO] provided_context (nil)
892
+ # Original context to use, if available
1059
893
  # @param [Hash{Symbol => Object}] options ({})
1060
894
  # @return [Hash]
1061
- def serialize(**options)
1062
- # FIXME: not setting provided_context now
895
+ def serialize(provided_context: nil, **options)
896
+ #log_debug("serlialize: generate context")
897
+ #log_debug("") {"=> context: #{inspect}"}
1063
898
  use_context = case provided_context
1064
899
  when String, RDF::URI
1065
900
  #log_debug "serlialize: reuse context: #{provided_context.inspect}"
1066
901
  provided_context.to_s
1067
- when Hash, Array
902
+ when Hash
903
+ #log_debug "serlialize: reuse context: #{provided_context.inspect}"
904
+ # If it has an @context entry use it, otherwise it is assumed to be the body of a context
905
+ provided_context.fetch('@context', provided_context)
906
+ when Array
1068
907
  #log_debug "serlialize: reuse context: #{provided_context.inspect}"
1069
908
  provided_context
909
+ when IO, StringIO
910
+ provided_context.rewind
911
+ JSON.load(provided_context).fetch('@context', {})
1070
912
  else
1071
- #log_debug("serlialize: generate context")
1072
- #log_debug("") {"=> context: #{inspect}"}
1073
913
  ctx = {}
1074
- ctx['@base'] = base.to_s if base && base != doc_base
914
+ ctx['@version'] = 1.1 if @processingMode == 'json-ld-1.1'
915
+ ctx['@base'] = base.to_s if base
1075
916
  ctx['@direction'] = default_direction.to_s if default_direction
1076
917
  ctx['@language'] = default_language.to_s if default_language
1077
918
  ctx['@vocab'] = vocab.to_s if vocab
1078
919
 
1079
920
  # Term Definitions
1080
- term_definitions.keys.each do |term|
1081
- defn = term_definitions[term].to_context_definition(self)
1082
- ctx[term] = defn if defn
921
+ term_definitions.each do |term, defn|
922
+ ctx[term] = defn.to_context_definition(self)
1083
923
  end
1084
-
1085
- #log_debug("") {"start_doc: context=#{ctx.inspect}"}
1086
924
  ctx
1087
925
  end
1088
926
 
1089
927
  # Return hash with @context, or empty
1090
- r = {}
1091
- r['@context'] = use_context unless use_context.nil? || use_context.empty?
1092
- r
928
+ use_context.nil? || use_context.empty? ? {} : {'@context' => use_context}
1093
929
  end
1094
930
 
1095
931
  ##
@@ -1202,7 +1038,7 @@ module JSON::LD
1202
1038
  # @param [Term, #to_s] term in unexpanded form
1203
1039
  # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>]
1204
1040
  def container(term)
1205
- return [term] if term == '@list'
1041
+ return Set[term] if term == '@list'
1206
1042
  term = find_definition(term)
1207
1043
  term ? term.container_mapping : Set.new
1208
1044
  end
@@ -1317,26 +1153,27 @@ module JSON::LD
1317
1153
  #
1318
1154
  # @param [String] value
1319
1155
  # A keyword, term, prefix:suffix or possibly relative IRI
1156
+ # @param [Boolean] as_string (false) transform RDF::Resource values to string
1157
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1158
+ # @param [Hash] defined
1159
+ # Used during Context Processing.
1320
1160
  # @param [Boolean] documentRelative (false)
1321
- # @param [Boolean] vocab (false)
1322
1161
  # @param [Hash] local_context
1323
1162
  # Used during Context Processing.
1324
- # @param [Hash] defined
1325
- # Used during Context Processing.
1326
- # @param [Boolean] as_string (false) transform RDF::Resource values to string
1163
+ # @param [Boolean] vocab (false)
1327
1164
  # @param [Hash{Symbol => Object}] options
1328
1165
  # @return [RDF::Resource, String]
1329
1166
  # IRI or String, if it's a keyword
1330
1167
  # @raise [JSON::LD::JsonLdError::InvalidIRIMapping] if the value cannot be expanded
1331
1168
  # @see https://www.w3.org/TR/json-ld11-api/#iri-expansion
1332
1169
  def expand_iri(value,
1170
+ as_string: false,
1171
+ base: nil,
1172
+ defined: nil,
1333
1173
  documentRelative: false,
1334
- vocab: false,
1335
1174
  local_context: nil,
1336
- defined: nil,
1337
- as_string: false,
1338
- **options
1339
- )
1175
+ vocab: false,
1176
+ **options)
1340
1177
  return (value && as_string ? value.to_s : value) unless value.is_a?(String)
1341
1178
 
1342
1179
  return value if KEYWORDS.include?(value)
@@ -1356,7 +1193,8 @@ module JSON::LD
1356
1193
  # If active context has a term definition for value, and the associated mapping is a keyword, return that keyword.
1357
1194
  # If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
1358
1195
  if (v_td = term_definitions[value]) && (vocab || KEYWORDS.include?(v_td.id))
1359
- return (as_string ? v_td.id.to_s : v_td.id)
1196
+ iri = base && v_td.id ? base.join(v_td.id) : v_td.id # vocab might be doc relative
1197
+ return (as_string ? iri.to_s : iri)
1360
1198
  end
1361
1199
 
1362
1200
  # If value contains a colon (:), it is either an absolute IRI or a compact IRI:
@@ -1389,18 +1227,32 @@ module JSON::LD
1389
1227
  end
1390
1228
  end
1391
1229
 
1230
+ iri = value.is_a?(RDF::URI) ? value : RDF::URI(value)
1392
1231
  result = if vocab && self.vocab
1393
1232
  # If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
1394
- self.vocab + value
1395
- elsif documentRelative && (base ||= self.base)
1396
- # Otherwise, if document relative is true, set value to the result of resolving value against the base IRI. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
1397
- value = RDF::URI(value)
1398
- value.absolute? ? value : RDF::URI(base).join(value)
1399
- elsif local_context && RDF::URI(value).relative?
1233
+ # Note that @vocab could still be relative to a document base
1234
+ (base && self.vocab.is_a?(RDF::URI) && self.vocab.relative? ? base.join(self.vocab) : self.vocab) + value
1235
+ elsif documentRelative
1236
+ if iri.absolute?
1237
+ iri
1238
+ elsif self.base.is_a?(RDF::URI) && self.base.absolute?
1239
+ self.base.join(iri)
1240
+ elsif self.base == false
1241
+ # No resollution of `@base: null`
1242
+ iri
1243
+ elsif base && self.base
1244
+ base.join(self.base).join(iri)
1245
+ elsif base
1246
+ base.join(iri)
1247
+ else
1248
+ # Returns a relative IRI in an odd case.
1249
+ iri
1250
+ end
1251
+ elsif local_context && iri.relative?
1400
1252
  # If local context is not null and value is not an absolute IRI, an invalid IRI mapping error has been detected and processing is aborted.
1401
1253
  raise JSON::LD::JsonLdError::InvalidIRIMapping, "not an absolute IRI: #{value}"
1402
1254
  else
1403
- RDF::URI(value)
1255
+ iri
1404
1256
  end
1405
1257
  result && as_string ? result.to_s : result
1406
1258
  end
@@ -1421,17 +1273,17 @@ module JSON::LD
1421
1273
  # Compacts an absolute IRI to the shortest matching term or compact IRI
1422
1274
  #
1423
1275
  # @param [RDF::URI] iri
1276
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1424
1277
  # @param [Object] value
1425
1278
  # Value, used to select among various maps for the same IRI
1426
- # @param [Boolean] vocab
1427
- # specifies whether the passed iri should be compacted using the active context's vocabulary mapping
1428
1279
  # @param [Boolean] reverse
1429
1280
  # specifies whether a reverse property is being compacted
1430
- # @param [Hash{Symbol => Object}] options ({})
1281
+ # @param [Boolean] vocab
1282
+ # specifies whether the passed iri should be compacted using the active context's vocabulary mapping
1431
1283
  #
1432
1284
  # @return [String] compacted form of IRI
1433
1285
  # @see https://www.w3.org/TR/json-ld11-api/#iri-compaction
1434
- def compact_iri(iri, value: nil, vocab: nil, reverse: false, **options)
1286
+ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil)
1435
1287
  return if iri.nil?
1436
1288
  iri = iri.to_s
1437
1289
 
@@ -1536,7 +1388,7 @@ module JSON::LD
1536
1388
  preferred_values = []
1537
1389
  preferred_values << '@reverse' if tl_value == '@reverse'
1538
1390
  if (tl_value == '@id' || tl_value == '@reverse') && value.is_a?(Hash) && value.has_key?('@id')
1539
- t_iri = compact_iri(value['@id'], vocab: true, document_relative: true)
1391
+ t_iri = compact_iri(value['@id'], vocab: true, base: base)
1540
1392
  if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
1541
1393
  preferred_values.concat(CONTAINERS_VOCAB_ID)
1542
1394
  else
@@ -1602,7 +1454,7 @@ module JSON::LD
1602
1454
 
1603
1455
  if !vocab
1604
1456
  # transform iri to a relative IRI using the document's base IRI
1605
- iri = remove_base(iri)
1457
+ iri = remove_base(self.base || base, iri)
1606
1458
  return iri
1607
1459
  else
1608
1460
  return iri
@@ -1622,26 +1474,24 @@ module JSON::LD
1622
1474
  # Value (literal or IRI) to be expanded
1623
1475
  # @param [Boolean] useNativeTypes (false) use native representations
1624
1476
  # @param [Boolean] rdfDirection (nil) decode i18n datatype if i18n-datatype
1477
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1625
1478
  # @param [Hash{Symbol => Object}] options
1626
1479
  #
1627
1480
  # @return [Hash] Object representation of value
1628
1481
  # @raise [RDF::ReaderError] if the iri cannot be expanded
1629
1482
  # @see https://www.w3.org/TR/json-ld11-api/#value-expansion
1630
- def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, **options)
1631
- #log_debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1632
-
1483
+ def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, base: nil, **options)
1633
1484
  td = term_definitions.fetch(property, TermDefinition.new(property))
1634
1485
 
1635
1486
  # If the active property has a type mapping in active context that is @id, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, and true for document relative.
1636
1487
  if value.is_a?(String) && td.type_mapping == '@id'
1637
1488
  #log_debug("") {"as relative IRI: #{value.inspect}"}
1638
- return {'@id' => expand_iri(value, documentRelative: true).to_s}
1489
+ return {'@id' => expand_iri(value, documentRelative: true, base: base).to_s}
1639
1490
  end
1640
1491
 
1641
1492
  # If active property has a type mapping in active context that is @vocab, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, true for vocab, and true for document relative.
1642
1493
  if value.is_a?(String) && td.type_mapping == '@vocab'
1643
- #log_debug("") {"as vocab IRI: #{value.inspect}"}
1644
- return {'@id' => expand_iri(value, vocab: true, documentRelative: true).to_s}
1494
+ return {'@id' => expand_iri(value, vocab: true, documentRelative: true, base: base).to_s}
1645
1495
  end
1646
1496
 
1647
1497
  value = RDF::Literal(value) if
@@ -1651,16 +1501,14 @@ module JSON::LD
1651
1501
 
1652
1502
  result = case value
1653
1503
  when RDF::URI, RDF::Node
1654
- #log_debug("URI | BNode") { value.to_s }
1655
1504
  {'@id' => value.to_s}
1656
1505
  when RDF::Literal
1657
- #log_debug("Literal") {"datatype: #{value.datatype.inspect}"}
1658
1506
  res = {}
1659
1507
  if value.datatype == RDF::URI(RDF.to_uri + "JSON") && processingMode('json-ld-1.1')
1660
1508
  # Value parsed as JSON
1661
1509
  # FIXME: MultiJson
1662
- res['@value'] = ::JSON.parse(value.object)
1663
1510
  res['@type'] = '@json'
1511
+ res['@value'] = ::JSON.parse(value.object)
1664
1512
  elsif value.datatype.start_with?("https://www.w3.org/ns/i18n#") && rdfDirection == 'i18n-datatype' && processingMode('json-ld-1.1')
1665
1513
  lang, dir = value.datatype.fragment.split('_')
1666
1514
  res['@value'] = value.to_s
@@ -1676,24 +1524,23 @@ module JSON::LD
1676
1524
  end
1677
1525
  res['@direction'] = dir
1678
1526
  elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype)
1679
- res['@value'] = value.object
1680
1527
  res['@type'] = uri(coerce(property)) if coerce(property)
1528
+ res['@value'] = value.object
1681
1529
  else
1682
1530
  value.canonicalize! if value.datatype == RDF::XSD.double
1683
- res['@value'] = value.to_s
1684
1531
  if coerce(property)
1685
1532
  res['@type'] = uri(coerce(property)).to_s
1686
1533
  elsif value.has_datatype?
1687
1534
  res['@type'] = uri(value.datatype).to_s
1688
1535
  elsif value.has_language? || language(property)
1689
1536
  res['@language'] = (value.language || language(property)).to_s
1690
- # FIXME: direction
1691
1537
  end
1538
+ res['@value'] = value.to_s
1692
1539
  end
1693
1540
  res
1694
1541
  else
1695
1542
  # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
1696
- res = {'@value' => value}
1543
+ res = {}
1697
1544
 
1698
1545
  if td.type_mapping && !CONTAINERS_ID_VOCAB.include?(td.type_mapping.to_s)
1699
1546
  res['@type'] = td.type_mapping.to_s
@@ -1704,10 +1551,9 @@ module JSON::LD
1704
1551
  res['@direction'] = direction if direction
1705
1552
  end
1706
1553
 
1707
- res
1554
+ res.merge('@value' => value)
1708
1555
  end
1709
1556
 
1710
- #log_debug("") {"=> #{result.inspect}"}
1711
1557
  result
1712
1558
  rescue ::JSON::ParserError => e
1713
1559
  raise JSON::LD::JsonLdError::InvalidJsonLiteral, e.message
@@ -1720,13 +1566,13 @@ module JSON::LD
1720
1566
  # Associated property used to find coercion rules
1721
1567
  # @param [Hash] value
1722
1568
  # Value (literal or IRI), in full object representation, to be compacted
1723
- # @param [Hash{Symbol => Object}] options
1569
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1724
1570
  #
1725
1571
  # @return [Hash] Object representation of value
1726
1572
  # @raise [JsonLdError] if the iri cannot be expanded
1727
1573
  # @see https://www.w3.org/TR/json-ld11-api/#value-compaction
1728
1574
  # FIXME: revisit the specification version of this.
1729
- def compact_value(property, value, **options)
1575
+ def compact_value(property, value, base: nil)
1730
1576
  #log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1731
1577
 
1732
1578
  indexing = index?(value) && container(property).include?('@index')
@@ -1737,7 +1583,7 @@ module JSON::LD
1737
1583
  when coerce(property) == '@id' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1738
1584
  # Compact an @id coercion
1739
1585
  #log_debug("") {" (@id & coerce)"}
1740
- compact_iri(value['@id'])
1586
+ compact_iri(value['@id'], base: base)
1741
1587
  when coerce(property) == '@vocab' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1742
1588
  # Compact an @id coercion
1743
1589
  #log_debug("") {" (@id & coerce & vocab)"}
@@ -1827,13 +1673,21 @@ module JSON::LD
1827
1673
  v.join(" ") + "]"
1828
1674
  end
1829
1675
 
1676
+ # Duplicate an active context, allowing it to be modified.
1830
1677
  def dup
1831
- # Also duplicate mappings, coerce and list
1832
1678
  that = self
1833
- ec = super
1679
+ ec = Context.new(unfrozen: true, **@options)
1680
+ ec.context_base = that.context_base
1681
+ ec.base = that.base unless that.base.nil?
1682
+ ec.default_direction = that.default_direction
1683
+ ec.default_language = that.default_language
1684
+ ec.previous_context = that.previous_context
1685
+ ec.processingMode = that.processingMode if that.instance_variable_get(:@processingModee)
1686
+ ec.vocab = that.vocab if that.vocab
1687
+
1834
1688
  ec.instance_eval do
1835
1689
  @term_definitions = that.term_definitions.dup
1836
- @iri_to_term = that.iri_to_term.dup
1690
+ @iri_to_term = that.iri_to_term
1837
1691
  end
1838
1692
  ec
1839
1693
  end
@@ -1879,20 +1733,11 @@ module JSON::LD
1879
1733
  bnode(namer.get_sym($1))
1880
1734
  else
1881
1735
  value = RDF::URI(value)
1882
- value.validate! if @options[:validate]
1883
- value.canonicalize! if @options[:canonicalize]
1884
- value = RDF::URI.intern(value, {}) if @options[:intern]
1736
+ #value.validate! if options[:validate]
1885
1737
  value
1886
1738
  end
1887
1739
  end
1888
1740
 
1889
- # Clear the provided context, used for testing
1890
- # @return [Context] self
1891
- def clear_provided_context
1892
- @provided_context = nil
1893
- self
1894
- end
1895
-
1896
1741
  # Keep track of allocated BNodes
1897
1742
  #
1898
1743
  # Don't actually use the name provided, to prevent name alias issues.
@@ -1933,7 +1778,7 @@ module JSON::LD
1933
1778
  # @return [Hash{String => Hash{String => String}}]
1934
1779
  # @todo May want to include @set along with container to allow selecting terms using @set over those without @set. May require adding some notion of value cardinality to compact_iri
1935
1780
  def inverse_context
1936
- @inverse_context ||= begin
1781
+ Context.inverse_cache[self.hash] ||= begin
1937
1782
  result = {}
1938
1783
  default_language = (self.default_language || '@none').downcase
1939
1784
  term_definitions.keys.sort do |a, b|
@@ -2028,10 +1873,11 @@ module JSON::LD
2028
1873
  ##
2029
1874
  # Removes a base IRI from the given absolute IRI.
2030
1875
  #
1876
+ # @param [String] base the base used for making `iri` relative
2031
1877
  # @param [String] iri the absolute IRI
2032
1878
  # @return [String]
2033
1879
  # the relative IRI if relative to base, otherwise the absolute IRI.
2034
- def remove_base(iri)
1880
+ def remove_base(base, iri)
2035
1881
  return iri unless base
2036
1882
  @base_and_parents ||= begin
2037
1883
  u = base
@@ -2141,5 +1987,232 @@ module JSON::LD
2141
1987
  end
2142
1988
  Array(container)
2143
1989
  end
1990
+
1991
+ # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
1992
+ class TermDefinition
1993
+ # @return [RDF::URI] IRI map
1994
+ attr_accessor :id
1995
+
1996
+ # @return [String] term name
1997
+ attr_accessor :term
1998
+
1999
+ # @return [String] Type mapping
2000
+ attr_accessor :type_mapping
2001
+
2002
+ # Base container mapping, without @set
2003
+ # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
2004
+ attr_reader :container_mapping
2005
+
2006
+ # @return [String] Term used for nest properties
2007
+ attr_accessor :nest
2008
+
2009
+ # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
2010
+ # @return [String] Language mapping
2011
+ attr_accessor :language_mapping
2012
+
2013
+ # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
2014
+ # @return ["ltr", "rtl"] direction_mapping
2015
+ attr_accessor :direction_mapping
2016
+
2017
+ # @return [Boolean] Reverse Property
2018
+ attr_accessor :reverse_property
2019
+
2020
+ # This is a simple term definition, not an expanded term definition
2021
+ # @return [Boolean]
2022
+ attr_accessor :simple
2023
+
2024
+ # Property used for data indexing; defaults to @index
2025
+ # @return [Boolean]
2026
+ attr_accessor :index
2027
+
2028
+ # Indicate that term may be used as a prefix
2029
+ attr_writer :prefix
2030
+
2031
+ # Term-specific context
2032
+ # @return [Hash{String => Object}]
2033
+ attr_accessor :context
2034
+
2035
+ # Term is protected.
2036
+ # @return [Boolean]
2037
+ attr_writer :protected
2038
+
2039
+ # This is a simple term definition, not an expanded term definition
2040
+ # @return [Boolean] simple
2041
+ def simple?; simple; end
2042
+
2043
+ # This is an appropriate term to use as the prefix of a compact IRI
2044
+ # @return [Boolean] simple
2045
+ def prefix?; @prefix; end
2046
+
2047
+ # Create a new Term Mapping with an ID
2048
+ # @param [String] term
2049
+ # @param [String] id
2050
+ # @param [String] type_mapping Type mapping
2051
+ # @param [Set<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
2052
+ # @param [String] language_mapping
2053
+ # Language mapping of term, `false` is used if there is an explicit language mapping for this term
2054
+ # @param ["ltr", "rtl"] direction_mapping
2055
+ # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
2056
+ # @param [Boolean] reverse_property
2057
+ # @param [Boolean] protected mark resulting context as protected
2058
+ # @param [String] nest term used for nest properties
2059
+ # @param [Boolean] simple
2060
+ # This is a simple term definition, not an expanded term definition
2061
+ # @param [Boolean] prefix
2062
+ # Term may be used as a prefix
2063
+ def initialize(term,
2064
+ id: nil,
2065
+ index: nil,
2066
+ type_mapping: nil,
2067
+ container_mapping: nil,
2068
+ language_mapping: nil,
2069
+ direction_mapping: nil,
2070
+ reverse_property: false,
2071
+ nest: nil,
2072
+ protected: nil,
2073
+ simple: false,
2074
+ prefix: nil,
2075
+ context: nil)
2076
+ @term = term
2077
+ @id = id.to_s unless id.nil?
2078
+ @index = index.to_s unless index.nil?
2079
+ @type_mapping = type_mapping.to_s unless type_mapping.nil?
2080
+ self.container_mapping = container_mapping
2081
+ @language_mapping = language_mapping unless language_mapping.nil?
2082
+ @direction_mapping = direction_mapping unless direction_mapping.nil?
2083
+ @reverse_property = reverse_property
2084
+ @protected = protected
2085
+ @nest = nest unless nest.nil?
2086
+ @simple = simple
2087
+ @prefix = prefix unless prefix.nil?
2088
+ @context = context unless context.nil?
2089
+ end
2090
+
2091
+ # Term is protected.
2092
+ # @return [Boolean]
2093
+ def protected?; !!@protected; end
2094
+
2095
+ # Set container mapping, from an array which may include @set
2096
+ def container_mapping=(mapping)
2097
+ mapping = case mapping
2098
+ when Set then mapping
2099
+ when Array then Set.new(mapping)
2100
+ when String then Set[mapping]
2101
+ when nil then Set.new
2102
+ else
2103
+ raise "Shouldn't happen with #{mapping.inspect}"
2104
+ end
2105
+ if @as_set = mapping.include?('@set')
2106
+ mapping = mapping.dup
2107
+ mapping.delete('@set')
2108
+ end
2109
+ @container_mapping = mapping
2110
+ @index ||= '@index' if mapping.include?('@index')
2111
+ end
2112
+
2113
+ ##
2114
+ # Output Hash or String definition for this definition considering @language and @vocab
2115
+ #
2116
+ # @param [Context] context
2117
+ # @return [String, Hash{String => Array[String], String}]
2118
+ def to_context_definition(context)
2119
+ cid = if context.vocab && id.start_with?(context.vocab)
2120
+ # Nothing to return unless it's the same as the vocab
2121
+ id == context.vocab ? context.vocab : id.to_s[context.vocab.length..-1]
2122
+ else
2123
+ # Find a term to act as a prefix
2124
+ iri, prefix = context.iri_to_term.detect {|i,p| id.to_s.start_with?(i.to_s)}
2125
+ iri && iri != id ? "#{prefix}:#{id.to_s[iri.length..-1]}" : id
2126
+ end
2127
+
2128
+ if simple?
2129
+ cid.to_s unless cid == term && context.vocab
2130
+ else
2131
+ defn = {}
2132
+ defn[reverse_property ? '@reverse' : '@id'] = cid.to_s unless cid == term && !reverse_property
2133
+ if type_mapping
2134
+ defn['@type'] = if KEYWORDS.include?(type_mapping)
2135
+ type_mapping
2136
+ else
2137
+ context.compact_iri(type_mapping, vocab: true)
2138
+ end
2139
+ end
2140
+
2141
+ cm = Array(container_mapping)
2142
+ cm << "@set" if as_set? && !cm.include?("@set")
2143
+ cm = cm.first if cm.length == 1
2144
+ defn['@container'] = cm unless cm.empty?
2145
+ # Language set as false to be output as null
2146
+ defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
2147
+ defn['@direction'] = (@direction_mapping ? @direction_mapping : nil) unless @direction_mapping.nil?
2148
+ defn['@context'] = @context if @context
2149
+ defn['@nest'] = @nest if @nest
2150
+ defn['@index'] = @index if @index
2151
+ defn['@prefix'] = @prefix unless @prefix.nil?
2152
+ defn
2153
+ end
2154
+ end
2155
+
2156
+ ##
2157
+ # Turn this into a source for a new instantiation
2158
+ # FIXME: context serialization
2159
+ # @return [String]
2160
+ def to_rb
2161
+ defn = [%(TermDefinition.new\(#{term.inspect})]
2162
+ %w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
2163
+ v = instance_variable_get("@#{acc}".to_sym)
2164
+ v = v.to_s if v.is_a?(RDF::Term)
2165
+ if acc == 'container_mapping'
2166
+ v = v.to_a
2167
+ v << '@set' if as_set?
2168
+ v = v.first if v.length <= 1
2169
+ end
2170
+ defn << "#{acc}: #{v.inspect}" if v
2171
+ end
2172
+ defn.join(', ') + ")"
2173
+ end
2174
+
2175
+ # If container mapping was defined along with @set
2176
+ # @return [Boolean]
2177
+ def as_set?; @as_set || false; end
2178
+
2179
+ # Check if term definitions are identical, modulo @protected
2180
+ # @return [Boolean]
2181
+ def ==(other)
2182
+ other.is_a?(TermDefinition) &&
2183
+ id == other.id &&
2184
+ term == other.term &&
2185
+ type_mapping == other.type_mapping &&
2186
+ container_mapping == other.container_mapping &&
2187
+ nest == other.nest &&
2188
+ language_mapping == other.language_mapping &&
2189
+ direction_mapping == other.direction_mapping &&
2190
+ reverse_property == other.reverse_property &&
2191
+ simple == other.simple &&
2192
+ index == other.index &&
2193
+ context == other.context &&
2194
+ prefix? == other.prefix? &&
2195
+ as_set? == other.as_set?
2196
+ end
2197
+
2198
+ def inspect
2199
+ v = %w([TD)
2200
+ v << "id=#{@id}"
2201
+ v << "index=#{index.inspect}" unless index.nil?
2202
+ v << "term=#{@term}"
2203
+ v << "rev" if reverse_property
2204
+ v << "container=#{container_mapping}" if container_mapping
2205
+ v << "as_set=#{as_set?.inspect}"
2206
+ v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
2207
+ v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
2208
+ v << "type=#{type_mapping}" unless type_mapping.nil?
2209
+ v << "nest=#{nest.inspect}" unless nest.nil?
2210
+ v << "simple=true" if @simple
2211
+ v << "protected=true" if @protected
2212
+ v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
2213
+ v << "has-context" unless context.nil?
2214
+ v.join(" ") + "]"
2215
+ end
2216
+ end
2144
2217
  end
2145
2218
  end