json-ld 3.0.2 → 3.1.0

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