json-ld 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +1 -1
  3. data/README.md +90 -53
  4. data/UNLICENSE +1 -1
  5. data/VERSION +1 -1
  6. data/bin/jsonld +4 -4
  7. data/lib/json/ld.rb +27 -10
  8. data/lib/json/ld/api.rb +325 -96
  9. data/lib/json/ld/compact.rb +75 -27
  10. data/lib/json/ld/conneg.rb +188 -0
  11. data/lib/json/ld/context.rb +677 -292
  12. data/lib/json/ld/expand.rb +240 -75
  13. data/lib/json/ld/flatten.rb +5 -3
  14. data/lib/json/ld/format.rb +19 -19
  15. data/lib/json/ld/frame.rb +135 -85
  16. data/lib/json/ld/from_rdf.rb +44 -17
  17. data/lib/json/ld/html/nokogiri.rb +151 -0
  18. data/lib/json/ld/html/rexml.rb +186 -0
  19. data/lib/json/ld/reader.rb +25 -5
  20. data/lib/json/ld/resource.rb +2 -2
  21. data/lib/json/ld/streaming_writer.rb +3 -1
  22. data/lib/json/ld/to_rdf.rb +47 -17
  23. data/lib/json/ld/utils.rb +4 -2
  24. data/lib/json/ld/writer.rb +75 -14
  25. data/spec/api_spec.rb +13 -34
  26. data/spec/compact_spec.rb +968 -9
  27. data/spec/conneg_spec.rb +373 -0
  28. data/spec/context_spec.rb +447 -53
  29. data/spec/expand_spec.rb +1872 -416
  30. data/spec/flatten_spec.rb +434 -47
  31. data/spec/frame_spec.rb +979 -344
  32. data/spec/from_rdf_spec.rb +305 -5
  33. data/spec/spec_helper.rb +177 -0
  34. data/spec/streaming_writer_spec.rb +4 -4
  35. data/spec/suite_compact_spec.rb +2 -2
  36. data/spec/suite_expand_spec.rb +14 -2
  37. data/spec/suite_flatten_spec.rb +10 -2
  38. data/spec/suite_frame_spec.rb +3 -2
  39. data/spec/suite_from_rdf_spec.rb +2 -2
  40. data/spec/suite_helper.rb +55 -20
  41. data/spec/suite_html_spec.rb +22 -0
  42. data/spec/suite_http_spec.rb +35 -0
  43. data/spec/suite_remote_doc_spec.rb +2 -2
  44. data/spec/suite_to_rdf_spec.rb +14 -3
  45. data/spec/support/extensions.rb +5 -1
  46. data/spec/test-files/test-4-input.json +3 -3
  47. data/spec/test-files/test-5-input.json +2 -2
  48. data/spec/test-files/test-8-framed.json +14 -18
  49. data/spec/to_rdf_spec.rb +606 -16
  50. data/spec/writer_spec.rb +5 -5
  51. metadata +144 -88
@@ -23,7 +23,10 @@ module JSON::LD
23
23
  referenced_once = {}
24
24
 
25
25
  value = nil
26
- ec = Context.new
26
+ ec = @context
27
+
28
+ # Create an entry for compound-literal node detection
29
+ compound_literal_subjects = {}
27
30
 
28
31
  # Create a map for node to object representation
29
32
 
@@ -32,14 +35,21 @@ module JSON::LD
32
35
  #log_debug("statement") { statement.to_nquads.chomp}
33
36
 
34
37
  name = statement.graph_name ? ec.expand_iri(statement.graph_name).to_s : '@default'
35
-
38
+
36
39
  # Create a graph entry as needed
37
40
  node_map = graph_map[name] ||= {}
41
+ compound_literal_subjects[name] ||= {}
42
+
38
43
  default_graph[name] ||= {'@id' => name} unless name == '@default'
39
-
44
+
40
45
  subject = ec.expand_iri(statement.subject).to_s
41
46
  node = node_map[subject] ||= {'@id' => subject}
42
47
 
48
+ # If predicate is rdf:datatype, note subject in compound literal subjects map
49
+ if @options[:rdfDirection] == 'compound-literal' && statement.predicate == RDF.to_uri + 'direction'
50
+ compound_literal_subjects[name][subject] ||= true
51
+ end
52
+
43
53
  # If object is an IRI or blank node identifier, and node map does not have an object member, create one and initialize its value to a new JSON object consisting of a single member @id whose value is set to object.
44
54
  node_map[statement.object.to_s] ||= {'@id' => statement.object.to_s} unless
45
55
  statement.object.literal?
@@ -50,8 +60,12 @@ module JSON::LD
50
60
  next
51
61
  end
52
62
 
53
- # Set value to the result of using the RDF to Object Conversion algorithm, passing object and use native types.
54
- value = ec.expand_value(nil, statement.object, useNativeTypes: useNativeTypes, log_depth: @options[:log_depth])
63
+ # Set value to the result of using the RDF to Object Conversion algorithm, passing object, rdfDirection, and use native types.
64
+ value = ec.expand_value(nil,
65
+ statement.object,
66
+ rdfDirection: @options[:rdfDirection],
67
+ useNativeTypes: useNativeTypes,
68
+ log_depth: @options[:log_depth])
55
69
 
56
70
  merge_value(node, statement.predicate.to_s, value)
57
71
 
@@ -77,6 +91,31 @@ module JSON::LD
77
91
 
78
92
  # For each name and graph object in graph map:
79
93
  graph_map.each do |name, graph_object|
94
+
95
+ # If rdfDirection is compound-literal, check referenced_once for entries from compound_literal_subjects
96
+ compound_literal_subjects.fetch(name, {}).keys.each do |cl|
97
+ node = referenced_once[cl][:node]
98
+ next unless node.is_a?(Hash)
99
+ property = referenced_once[cl][:property]
100
+ value = referenced_once[cl][:value]
101
+ cl_node = graph_map[name].delete(cl)
102
+ next unless cl_node.is_a?(Hash)
103
+ node[property].select do |v|
104
+ next unless v['@id'] == cl
105
+ v.delete('@id')
106
+ v['@value'] = cl_node[RDF.value.to_s].first['@value']
107
+ if cl_node[RDF.to_uri.to_s + 'language']
108
+ lang = cl_node[RDF.to_uri.to_s + 'language'].first['@value']
109
+ if lang !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
110
+ warn "i18n datatype language must be valid BCP47: #{lang.inspect}"
111
+ end
112
+ v['@language'] = lang
113
+ end
114
+ v['@direction'] = cl_node[RDF.to_uri.to_s + 'direction'].first['@value']
115
+ end
116
+ end
117
+
118
+ # Skip to next graph, unless this one has lists
80
119
  next unless nil_var = graph_object[RDF.nil.to_s]
81
120
 
82
121
  # For each item usage in the usages member of nil, perform the following steps:
@@ -101,18 +140,6 @@ module JSON::LD
101
140
  node, property, head = node_usage[:node], node_usage[:property], node_usage[:value]
102
141
  end
103
142
 
104
- # If property equals rdf:first, i.e., the detected list is nested inside another list
105
- #if property == RDF.first.to_s
106
- # # and the value of the @id of node equals rdf:nil, i.e., the detected list is empty, continue with the next usage item. The rdf:nil node cannot be converted to a list object as it would result in a list of lists, which isn't supported.
107
- # next if node['@id'] == RDF.nil.to_s
108
- #
109
- # # Otherwise, the list consists of at least one item. We preserve the head node and transform the rest of the linked list to a list object
110
- # head_id = head['@id']
111
- # head = graph_object[head_id]
112
- # head = Array(head[RDF.rest.to_s]).first
113
- # list.pop; list_nodes.pop
114
- #end
115
-
116
143
  head.delete('@id')
117
144
  head['@list'] = list.reverse
118
145
  list_nodes.each {|node_id| graph_object.delete(node_id)}
@@ -0,0 +1,151 @@
1
+ module JSON::LD
2
+ class API
3
+ ##
4
+ # Nokogiri implementation of an HTML parser.
5
+ #
6
+ # @see http://nokogiri.org/
7
+ module Nokogiri
8
+ ##
9
+ # Returns the name of the underlying XML library.
10
+ #
11
+ # @return [Symbol]
12
+ def self.library
13
+ :nokogiri
14
+ end
15
+
16
+ # Proxy class to implement uniform element accessors
17
+ class NodeProxy
18
+ attr_reader :node
19
+ attr_reader :parent
20
+
21
+ def initialize(node, parent = nil)
22
+ @node = node
23
+ @parent = parent
24
+ end
25
+
26
+ ##
27
+ # Return xml:base on element, if defined
28
+ #
29
+ # @return [String]
30
+ def base
31
+ @node.attribute_with_ns("base", RDF::XML.to_s) || @node.attribute('xml:base')
32
+ end
33
+
34
+ def display_path
35
+ @display_path ||= begin
36
+ path = []
37
+ path << parent.display_path if parent
38
+ path << @node.name
39
+ case @node
40
+ when ::Nokogiri::XML::Element then path.join("/")
41
+ when ::Nokogiri::XML::Attr then path.join("@")
42
+ else path.join("?")
43
+ end
44
+ end
45
+ end
46
+
47
+ ##
48
+ # Return true of all child elements are text
49
+ #
50
+ # @return [Array<:text, :element, :attribute>]
51
+ def text_content?
52
+ @node.children.all? {|c| c.text?}
53
+ end
54
+
55
+ ##
56
+ # Children of this node
57
+ #
58
+ # @return [NodeSetProxy]
59
+ def children
60
+ NodeSetProxy.new(@node.children, self)
61
+ end
62
+
63
+ # Ancestors of this element, in order
64
+ def ancestors
65
+ @ancestors ||= parent ? parent.ancestors + [parent] : []
66
+ end
67
+
68
+ ##
69
+ # Inner text of an element. Decode Entities
70
+ #
71
+ # @return [String]
72
+ #def inner_text
73
+ # coder = HTMLEntities.new
74
+ # coder.decode(@node.inner_text)
75
+ #end
76
+
77
+ def attribute_nodes
78
+ @attribute_nodes ||= NodeSetProxy.new(@node.attribute_nodes, self)
79
+ end
80
+
81
+ def xpath(*args)
82
+ @node.xpath(*args).map do |n|
83
+ # Get node ancestors
84
+ parent = n.ancestors.reverse.inject(nil) do |p,node|
85
+ NodeProxy.new(node, p)
86
+ end
87
+ NodeProxy.new(n, parent)
88
+ end
89
+ end
90
+
91
+ ##
92
+ # Proxy for everything else to @node
93
+ def method_missing(method, *args)
94
+ @node.send(method, *args)
95
+ end
96
+ end
97
+
98
+ ##
99
+ # NodeSet proxy
100
+ class NodeSetProxy
101
+ attr_reader :node_set
102
+ attr_reader :parent
103
+
104
+ def initialize(node_set, parent)
105
+ @node_set = node_set
106
+ @parent = parent
107
+ end
108
+
109
+ ##
110
+ # Return a proxy for each child
111
+ #
112
+ # @yield child
113
+ # @yieldparam [NodeProxy]
114
+ def each
115
+ @node_set.each do |c|
116
+ yield NodeProxy.new(c, parent)
117
+ end
118
+ end
119
+
120
+ ##
121
+ # Proxy for everything else to @node_set
122
+ def method_missing(method, *args)
123
+ @node_set.send(method, *args)
124
+ end
125
+ end
126
+
127
+ ##
128
+ # Initializes the underlying XML library.
129
+ #
130
+ # @param [Hash{Symbol => Object}] options
131
+ # @return [NodeProxy] of root element
132
+ def initialize_html(input, options = {})
133
+ require 'nokogiri' unless defined?(::Nokogiri)
134
+ doc = case input
135
+ when ::Nokogiri::HTML::Document, ::Nokogiri::XML::Document
136
+ input
137
+ else
138
+ begin
139
+ require 'nokogumbo' unless defined?(::Nokogumbo)
140
+ input = input.read if input.respond_to?(:read)
141
+ ::Nokogiri::HTML5(input.dup.force_encoding('utf-8'), max_parse_errors: 1000)
142
+ rescue LoadError
143
+ ::Nokogiri::HTML.parse(input, 'utf-8')
144
+ end
145
+ end
146
+
147
+ NodeProxy.new(doc.root) if doc && doc.root
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,186 @@
1
+ require 'htmlentities'
2
+
3
+ module JSON::LD
4
+ class API
5
+ ##
6
+ # REXML implementation of an XML parser.
7
+ #
8
+ # @see http://www.germane-software.com/software/rexml/
9
+ module REXML
10
+ ##
11
+ # Returns the name of the underlying XML library.
12
+ #
13
+ # @return [Symbol]
14
+ def self.library
15
+ :rexml
16
+ end
17
+
18
+ # Proxy class to implement uniform element accessors
19
+ class NodeProxy
20
+ attr_reader :node
21
+ attr_reader :parent
22
+
23
+ def initialize(node, parent = nil)
24
+ @node = node
25
+ @parent = parent
26
+ end
27
+
28
+ ##
29
+ # Return xml:base on element, if defined
30
+ #
31
+ # @return [String]
32
+ def base
33
+ @node.attribute("base", RDF::XML.to_s) || @node.attribute('xml:base')
34
+ end
35
+
36
+ def display_path
37
+ @display_path ||= begin
38
+ path = []
39
+ path << parent.display_path if parent
40
+ path << @node.name
41
+ case @node
42
+ when ::REXML::Element then path.join("/")
43
+ when ::REXML::Attribute then path.join("@")
44
+ else path.join("?")
45
+ end
46
+ end
47
+ end
48
+
49
+ ##
50
+ # Return true of all child elements are text
51
+ #
52
+ # @return [Array<:text, :element, :attribute>]
53
+ def text_content?
54
+ @node.children.all? {|c| c.is_a?(::REXML::Text)}
55
+ end
56
+
57
+ ##
58
+ # Children of this node
59
+ #
60
+ # @return [NodeSetProxy]
61
+ def children
62
+ NodeSetProxy.new(@node.children, self)
63
+ end
64
+
65
+ # Ancestors of this element, in order
66
+ def ancestors
67
+ @ancestors ||= parent ? parent.ancestors + [parent] : []
68
+ end
69
+
70
+ ##
71
+ # Inner text of an element
72
+ #
73
+ # @see http://apidock.com/ruby/REXML/Element/get_text#743-Get-all-inner-texts
74
+ # @return [String]
75
+ def inner_text
76
+ coder = HTMLEntities.new
77
+ ::REXML::XPath.match(@node,'.//text()').map { |e|
78
+ coder.decode(e)
79
+ }.join
80
+ end
81
+
82
+ ##
83
+ # Inner text of an element
84
+ #
85
+ # @see http://apidock.com/ruby/REXML/Element/get_text#743-Get-all-inner-texts
86
+ # @return [String]
87
+ def inner_html
88
+ @node.children.map(&:to_s).join
89
+ end
90
+
91
+ def attribute_nodes
92
+ attrs = @node.attributes.dup.keep_if do |name, attr|
93
+ !name.start_with?('xmlns')
94
+ end
95
+ @attribute_nodes ||= (attrs.empty? ? attrs : NodeSetProxy.new(attrs, self))
96
+ end
97
+
98
+ ##
99
+ # Node type accessors
100
+ #
101
+ # @return [Boolean]
102
+ def text?
103
+ @node.is_a?(::REXML::Text)
104
+ end
105
+
106
+ def element?
107
+ @node.is_a?(::REXML::Element)
108
+ end
109
+
110
+ def blank?
111
+ @node.is_a?(::REXML::Text) && @node.empty?
112
+ end
113
+
114
+ def to_s; @node.to_s; end
115
+
116
+ def xpath(*args)
117
+ ::REXML::XPath.match(@node, *args).map do |n|
118
+ NodeProxy.new(n, parent)
119
+ end
120
+ end
121
+
122
+ def at_xpath(*args)
123
+ xpath(*args).first
124
+ end
125
+
126
+ ##
127
+ # Proxy for everything else to @node
128
+ def method_missing(method, *args)
129
+ @node.send(method, *args)
130
+ end
131
+ end
132
+
133
+ ##
134
+ # NodeSet proxy
135
+ class NodeSetProxy
136
+ attr_reader :node_set
137
+ attr_reader :parent
138
+
139
+ def initialize(node_set, parent)
140
+ @node_set = node_set
141
+ @parent = parent
142
+ end
143
+
144
+ ##
145
+ # Return a proxy for each child
146
+ #
147
+ # @yield child
148
+ # @yieldparam [NodeProxy]
149
+ def each
150
+ @node_set.each do |c|
151
+ yield NodeProxy.new(c, parent)
152
+ end
153
+ end
154
+
155
+ ##
156
+ def to_html
157
+ node_set.map(&:to_s).join("")
158
+ end
159
+
160
+ ##
161
+ # Proxy for everything else to @node_set
162
+ def method_missing(method, *args)
163
+ @node_set.send(method, *args)
164
+ end
165
+ end
166
+
167
+ ##
168
+ # Initializes the underlying XML library.
169
+ #
170
+ # @param [Hash{Symbol => Object}] options
171
+ # @return [NodeProxy] of document root
172
+ def initialize_html(input, options = {})
173
+ require 'rexml/document' unless defined?(::REXML)
174
+ doc = case input
175
+ when ::REXML::Document
176
+ input
177
+ else
178
+ # Only parse as XML, no HTML mode
179
+ ::REXML::Document.new(input.respond_to?(:read) ? input.read : input.to_s)
180
+ end
181
+
182
+ NodeProxy.new(doc.root) if doc && doc.root
183
+ end
184
+ end
185
+ end
186
+ end
@@ -4,7 +4,7 @@ module JSON::LD
4
4
  ##
5
5
  # A JSON-LD parser in Ruby.
6
6
  #
7
- # @see http://json-ld.org/spec/ED/20110507/
7
+ # @see https://www.w3.org/TR/json-ld11-api
8
8
  # @author [Gregg Kellogg](http://greggkellogg.net/)
9
9
  class Reader < RDF::Reader
10
10
  format Format
@@ -20,12 +20,32 @@ module JSON::LD
20
20
  datatype: RDF::URI,
21
21
  on: ["--expand-context CONTEXT"],
22
22
  description: "Context to use when expanding.") {|arg| RDF::URI(arg)},
23
+ RDF::CLI::Option.new(
24
+ symbol: :extractAllScripts,
25
+ datatype: TrueClass,
26
+ default: false,
27
+ control: :checkbox,
28
+ on: ["--[no-]extract-all-scripts"],
29
+ description: "If set to true, when extracting JSON-LD script elements from HTML, unless a specific fragment identifier is targeted, extracts all encountered JSON-LD script elements using an array form, if necessary.") {|arg| RDF::URI(arg)},
30
+ RDF::CLI::Option.new(
31
+ symbol: :lowercaseLanguage,
32
+ datatype: TrueClass,
33
+ control: :checkbox,
34
+ on: ["--[no-]lowercase-language"],
35
+ description: "By default, language tags are left as is. To normalize to lowercase, set this option to `true`."),
23
36
  RDF::CLI::Option.new(
24
37
  symbol: :processingMode,
25
38
  datatype: %w(json-ld-1.0 json-ld-1.1),
26
39
  control: :radio,
27
40
  on: ["--processingMode MODE", %w(json-ld-1.0 json-ld-1.1)],
28
41
  description: "Set Processing Mode (json-ld-1.0 or json-ld-1.1)"),
42
+ RDF::CLI::Option.new(
43
+ symbol: :rdfDirection,
44
+ datatype: %w(i18n-datatype compound-literal),
45
+ default: 'null',
46
+ control: :select,
47
+ on: ["--rdf-direction DIR", %w(i18n-datatype compound-literal)],
48
+ description: "How to serialize literal direction (i18n-datatype compound-literal)") {|arg| RDF::URI(arg)},
29
49
  ]
30
50
  end
31
51
 
@@ -39,7 +59,7 @@ module JSON::LD
39
59
  # @yieldparam [RDF::Reader] reader
40
60
  # @yieldreturn [void] ignored
41
61
  # @raise [RDF::ReaderError] if the JSON document cannot be loaded
42
- def initialize(input = $stdin, options = {}, &block)
62
+ def initialize(input = $stdin, **options, &block)
43
63
  options[:base_uri] ||= options[:base]
44
64
  super do
45
65
  @options[:base] ||= base_uri.to_s if base_uri
@@ -65,7 +85,7 @@ module JSON::LD
65
85
  # @private
66
86
  # @see RDF::Reader#each_statement
67
87
  def each_statement(&block)
68
- JSON::LD::API.toRdf(@doc, @options, &block)
88
+ JSON::LD::API.toRdf(@doc, **@options, &block)
69
89
  rescue ::JSON::ParserError, ::JSON::LD::JsonLdError => e
70
90
  log_fatal("Failed to parse input document: #{e.message}", exception: RDF::ReaderError)
71
91
  end
@@ -75,8 +95,8 @@ module JSON::LD
75
95
  # @see RDF::Reader#each_triple
76
96
  def each_triple(&block)
77
97
  if block_given?
78
- JSON::LD::API.toRdf(@doc, @options) do |statement|
79
- yield *statement.to_triple
98
+ JSON::LD::API.toRdf(@doc, **@options) do |statement|
99
+ yield(*statement.to_triple)
80
100
  end
81
101
  end
82
102
  enum_for(:each_triple)