json-ld 3.1.2 → 3.1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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