lib/rdf/rdfa/reader.rb in rdf-rdfa-0.3.5.1 vs lib/rdf/rdfa/reader.rb in rdf-rdfa-0.3.6

- old
+ new

@@ -10,10 +10,12 @@ # @see http://www.w3.org/TR/2011/WD-xhtml-rdfa-20110331/ XHTML+RDFa 1.1 # # @author [Gregg Kellogg](http://kellogg-assoc.com/) class Reader < RDF::Reader format Format + include Expansion + XHTML = "http://www.w3.org/1999/xhtml" SafeCURIEorCURIEorURI = { :"rdfa1.0" => [:term, :safe_curie, :uri, :bnode], :"rdfa1.1" => [:safe_curie, :curie, :term, :uri, :bnode], @@ -55,80 +57,104 @@ attr_reader :base_uri # The Recursive Baggage # @private class EvaluationContext # :nodoc: + ## # The base. # # This will usually be the URL of the document being processed, # but it could be some other URL, set by some other mechanism, # such as the (X)HTML base element. The important thing is that it establishes # a URL against which relative paths can be resolved. # - # @return [URI] + # @attr [RDF::URI] attr :base, true + + ## # The parent subject. # # The initial value will be the same as the initial value of base, # but it will usually change during the course of processing. # - # @return [URI] + # @attr [RDF::URI] attr :parent_subject, true + + ## # The parent object. # # In some situations the object of a statement becomes the subject of any nested statements, # and this property is used to convey this value. # Note that this value may be a bnode, since in some situations a number of nested statements # are grouped together on one bnode. # This means that the bnode must be set in the containing statement and passed down, # and this property is used to convey this value. # - # @return URI + # @attr [RDF::URI] attr :parent_object, true + + ## # A list of current, in-scope URI mappings. # - # @return [Hash{Symbol => String}] + # @attr [Hash{Symbol => String}] attr :uri_mappings, true + + ## # A list of current, in-scope Namespaces. This is the subset of uri_mappings # which are defined using xmlns. # - # @return [Hash{String => Namespace}] + # @attr [Hash{String => Namespace}] attr :namespaces, true + + ## # A list of incomplete triples. # # A triple can be incomplete when no object resource # is provided alongside a predicate that requires a resource (i.e., @rel or @rev). # The triples can be completed when a resource becomes available, # which will be when the next subject is specified (part of the process called chaining). # - # @return [Array<Array<URI, Resource>>] + # @attr [Array<Array<RDF::URI, RDF::Resource>>] attr :incomplete_triples, true + + ## # The language. Note that there is no default language. # - # @return [Symbol] + # @attr [Symbol] attr :language, true + + ## # The term mappings, a list of terms and their associated URIs. # # This specification does not define an initial list. # Host Languages may define an initial list. # If a Host Language provides an initial list, it should do so via an RDFa Profile document. # - # @return [Hash{Symbol => URI}] + # @attr [Hash{Symbol => RDF::URI}] attr :term_mappings, true + + ## # The default vocabulary # # A value to use as the prefix URI when a term is used. # This specification does not define an initial setting for the default vocabulary. # Host Languages may define an initial setting. # - # @return [URI] + # @attr [RDF::URI] attr :default_vocabulary, true + ## + # collections + # + # A hash associating collections with properties. + # @attr [Hash{RDF::URI => Array<RDF::Resource>}] + attr :collection_mappings, true + # @param [RDF::URI] base # @param [Hash] host_defaults - # @option host_defaults [Hash{String => URI}] :term_mappings Hash of NCName => URI - # @option host_defaults [Hash{String => URI}] :vocabulary Hash of prefix => URI + # @option host_defaults [Hash{String => RDF::URI}] :term_mappings Hash of NCName => URI + # @option host_defaults [Hash{String => RDF::URI}] :vocabulary Hash of prefix => URI def initialize(base, host_defaults) # Initialize the evaluation context, [5.1] @base = base @parent_subject = @base @parent_object = nil @@ -146,17 +172,21 @@ def initialize_copy(from) # clone the evaluation context correctly @uri_mappings = from.uri_mappings.clone @incomplete_triples = from.incomplete_triples.clone @namespaces = from.namespaces.clone + @collection_mappings = from.collection_mappings # Don't clone end def inspect - v = %w(base parent_subject parent_object language default_vocabulary).map {|a| "#{a}='#{self.send(a).inspect}'"} + v = ['base', 'parent_subject', 'parent_object', 'language', 'default_vocabulary'].map do |a| + "#{a}='#{self.send(a).inspect}'" + end v << "uri_mappings[#{uri_mappings.keys.length}]" v << "incomplete_triples[#{incomplete_triples.length}]" v << "term_mappings[#{term_mappings.keys.length}]" + v << "collections[#{collection_mappings.keys.length}]" if collection_mappings v.join(", ") end end ## @@ -170,24 +200,26 @@ # the encoding of the input stream (Ruby 1.9+) # @option options [Boolean] :validate (false) # whether to validate the parsed statements and values # @option options [Boolean] :canonicalize (false) # whether to canonicalize parsed literals + # @option options [Boolean] :expand (false) + # whether to perform RDFS expansion on the resulting graph # @option options [Boolean] :intern (true) # whether to intern all parsed URIs # @option options [Hash] :prefixes (Hash.new) # the prefix mappings to use (not supported by all readers) # @option options [#to_s] :base_uri (nil) # the base URI to use when resolving relative URIs # @option options [:xml1, :xhtml1, :xhtml5, :html4, :html5, :svg] :host_language (:xhtml1) # Host Language # @option options [:"rdfa1.0", :"rdfa1.1"] :version (:"rdfa1.1") # Parser version information - # @option options [Graph] :processor_graph (nil) + # @option options [RDF::Writable] :processor_graph (nil) # Graph to record information, warnings and errors. - # @option options [Repository] :profile_repository (nil) - # Repository to save profile graphs. + # @option options [Repository] :vocab_repository (nil) + # Repository to save loaded vocabularies. # @option options [Array] :debug # Array to place debug messages # @return [reader] # @yield [reader] `self` # @yieldparam [RDF::Reader] reader @@ -252,11 +284,10 @@ add_info(@doc, "version = #{@version}, host_language = #{@host_language}") block.call(self) if block_given? end - self.profile_repository = options[:profile_repository] if options[:profile_repository] end # Determine the host language and/or version from options and the input document def detect_host_language_version(input, options) @host_language = options[:host_language] ? options[:host_language].to_sym : nil @@ -339,37 +370,34 @@ end @host_language ||= :xml1 end - # @return [RDF::Repository] - def profile_repository - Profile.repository - end - - # @param [RDF::Repository] repo - # @return [RDF::Repository] - def profile_repository=(repo) - Profile.repository = repo - end - ## # Iterates the given block for each RDF statement in the input. # + # Reads to graph and performs expansion if required. + # # @yield [statement] # @yieldparam [RDF::Statement] statement # @return [void] def each_statement(&block) - @callback = block + if @options[:expand] + @options[:expand] = false + expand.each_statement(&block) + @options[:expand] = true + else + @callback = block - # Add prefix definitions from host defaults - @host_defaults[:uri_mappings].each_pair do |prefix, value| - prefix(prefix, value) - end + # Add prefix definitions from host defaults + @host_defaults[:uri_mappings].each_pair do |prefix, value| + prefix(prefix, value) + end - # parse - parse_whole_document(@doc, @base_uri) + # parse + parse_whole_document(@doc, @base_uri) + end end ## # Iterates the given block for each RDF triple in the input. # @@ -400,13 +428,16 @@ end end # Add debug event to debug array, if specified # - # @param [XML Node, any] node:: XML Node or string for showing context + # @param [Nokogiri::XML::Node, #to_s] node:: XML Node or string for showing context # @param [String] message:: - def add_debug(node, message) + # @yieldreturn [String] appended to message, to allow for lazy-evaulation of message + def add_debug(node, message = "") + return unless ::RDF::RDFa.debug? || @debug + message = message + yield if block_given? add_processor_message(node, message, RDF::RDFA.Info) end def add_info(node, message, process_class = RDF::RDFA.Info) add_processor_message(node, message, process_class) @@ -437,23 +468,22 @@ end end # add a statement, object can be literal or URI or bnode # - # @param [Nokogiri::XML::Node, any] node:: XML Node or string for showing context - # @param [URI, BNode] subject:: the subject of the statement - # @param [URI] predicate:: the predicate of the statement - # @param [URI, BNode, Literal] object:: the object of the statement - # @return [Statement]:: Added statement - # @raise [ReaderError]:: Checks parameter types and raises if they are incorrect if parsing mode is _validate_. + # @param [Nokogiri::XML::Node, #to_s] node:: XML Node or string for showing context + # @param [RDF::URI, RDF::BNode] subject:: the subject of the statement + # @param [RDF::URI] predicate:: the predicate of the statement + # @param [URI, RDF::BNode, RDF::Literal] object:: the object of the statement + # @return [RDF::Statement]:: Added statement + # @raise [RDF::ReaderError]:: Checks parameter types and raises if they are incorrect if parsing mode is _validate_. def add_triple(node, subject, predicate, object) statement = RDF::Statement.new(subject, predicate, object) add_info(node, "statement: #{RDF::NTriples.serialize(statement)}") @callback.call(statement) end - # Parsing an RDFa document (this is *not* the recursive method) def parse_whole_document(doc, base) # find if the document has a base element case @host_language when :xhtml1, :xhtml5, :html4, :html5 @@ -466,20 +496,20 @@ if (base) # Strip any fragment from base base = base.to_s.split("#").first base = uri(base) - add_debug("", "parse_whole_doc: base='#{base}'") + add_debug("") {"parse_whole_doc: base='#{base}'"} end # initialize the evaluation context with the appropriate base evaluation_context = EvaluationContext.new(base, @host_defaults) if @version != :"rdfa1.0" # Process default vocabularies process_profile(doc.root, @host_defaults[:profiles]) do |which, value| - add_debug(doc.root, "parse_whole_document, #{which}: #{value.inspect}") + add_debug(doc.root) { "parse_whole_document, #{which}: #{value.inspect}"} case which when :uri_mappings then evaluation_context.uri_mappings.merge!(value) when :term_mappings then evaluation_context.term_mappings.merge!(value) when :default_vocabulary then evaluation_context.default_vocabulary = value end @@ -592,11 +622,11 @@ if element.nil? add_error(element, "Can't parse nil element") return nil end - add_debug(element, "traverse, ec: #{evaluation_context.inspect}") + add_debug(element) { "ec: #{evaluation_context.inspect}" } # local variables [7.5 Step 1] recurse = true skip = false new_subject = nil @@ -605,13 +635,12 @@ namespaces = evaluation_context.namespaces.clone incomplete_triples = [] language = evaluation_context.language term_mappings = evaluation_context.term_mappings.clone default_vocabulary = evaluation_context.default_vocabulary + collection_mappings = evaluation_context.collection_mappings - current_object_literal = nil # XXX Not explicit - # shortcut attrs = element.attributes about = attrs['about'] src = attrs['src'] @@ -627,87 +656,65 @@ typeof = attrs['typeof'].to_s.strip if attrs.has_key?('typeof') datatype = attrs['datatype'].to_s if attrs['datatype'] content = attrs['content'].to_s if attrs['content'] rel = attrs['rel'].to_s.strip if attrs['rel'] rev = attrs['rev'].to_s.strip if attrs['rev'] - profiles = attrs['profile'].to_s.split(/\s/) # In-scope profiles in order for passing to XMLLiteral - attrs = { - :about => about, - :src => src, - :resource => resource, - :href => href, - :vocab => vocab, - :base => xml_base, - :property => property, - :typeof => typeof, - :datatype => datatype, - :rel => rel, - :rev => rev, - :profiles => (profiles.empty? ? nil : profiles), - }.select{|k,v| v} - - add_debug(element, "traverse " + attrs.map{|a| "#{a.first}: #{a.last}"}.join(", ")) unless attrs.empty? + # Collections: + # @member + # an attribute (value ignored) used to indicate that the object associated with a + # @rel or @property attribute on the same element is to be added to the collection + # for that property. Causes a collection to be created if it does not already exist. + member = attrs['member'].to_s.strip if attrs.has_key?('member') - # Local term mappings [7.5 Steps 2] - # Next the current element is parsed for any updates to the local term mappings and local list of URI mappings via @profile. - # If @profile is present, its value is processed as defined in RDFa Profiles. - unless @version == :"rdfa1.0" - begin - process_profile(element, profiles) do |which, value| - add_debug(element, "[Step 2] traverse, #{which}: #{value.inspect}") - case which - when :uri_mappings - value.each do |k, v| - if k.to_s.match(NC_REGEXP) - uri_mappings[k] = v - else - add_error(element, "[Step 2] traverse: Prefix #{k.to_s.inspect} does not match NCName production") - end - end - when :term_mappings - value.each do |k, v| - if k.to_s.match(NC_REGEXP) - term_mappings[k] = v - else - add_error(element, "[Step 2] traverse: Term #{k.to_s.inspect} does not match NCName production") - end - end - when :default_vocabulary - default_vocabulary = value - end - end - rescue - # Skip this element and all sub-elements - # If any referenced RDFa Profile is not available, then the current element and its children must not place any - # triples in the default graph . - raise if validate? - return - end - end + add_debug(element) do + attrs = { + :about => about, + :src => src, + :resource => resource, + :href => href, + :vocab => vocab, + :base => xml_base, + :property => property, + :typeof => typeof, + :datatype => datatype, + :rel => rel, + :rev => rev, + :member => member, + }.select {|k,v| v} - # Default vocabulary [7.5 Step 3] + "attrs " + attrs.map {|a| "#{a.first}: #{a.last}"}.join(", ") + end unless attrs.empty? + + # Default vocabulary [7.5 Step 2] # Next the current element is examined for any change to the default vocabulary via @vocab. # If @vocab is present and contains a value, its value updates the local default vocabulary. # If the value is empty, then the local default vocabulary must be reset to the Host Language defined default. unless vocab.nil? default_vocabulary = if vocab.to_s.empty? # Set default_vocabulary to host language default - add_debug(element, "[Step 3] traverse, reset default_vocaulary to #{@host_defaults.fetch(:vocabulary, nil).inspect}") + add_debug(element) { + "[Step 3] reset default_vocaulary to #{@host_defaults.fetch(:vocabulary, nil).inspect}" + } @host_defaults.fetch(:vocabulary, nil) else + # Generate a triple indicating that the vocabulary is used + add_triple(element, base, RDF::RDFA.hasVocabulary, uri(vocab)) + uri(vocab) end - add_debug(element, "[Step 3] traverse, default_vocaulary: #{default_vocabulary.inspect}") + add_debug(element) { + "[Step 2] default_vocaulary: #{default_vocabulary.inspect}" + } end - # Local term mappings [7.5 Steps 4] + # Local term mappings [7.5 Step 3] # Next, the current element is then examined for URI mapping s and these are added to the local list of URI mappings. # Note that a URI mapping will simply overwrite any current mapping in the list that has the same name extract_mappings(element, uri_mappings, namespaces) - # Language information [7.5 Step 5] + # Language information [7.5 Step 4] # From HTML5 [3.2.3.3] # If both the lang attribute in no namespace and the lang attribute in the XML namespace are set # on an element, user agents must use the lang attribute in the XML namespace, and the lang # attribute in no namespace must be ignored for the purposes of determining the element's # language. @@ -722,11 +729,11 @@ element.at_xpath("@lang").to_s else language end language = nil if language.to_s.empty? - add_debug(element, "HTML5 [3.2.3.3] traverse, lang: #{language || 'nil'}") if language + add_debug(element, "HTML5 [3.2.3.3] lang: #{language || 'nil'}") if language # rels and revs rels = process_uris(element, rel, evaluation_context, base, :uri_mappings => uri_mappings, :term_mappings => term_mappings, @@ -736,14 +743,16 @@ :uri_mappings => uri_mappings, :term_mappings => term_mappings, :vocab => default_vocabulary, :restrictions => TERMorCURIEorAbsURI[@version]) - add_debug(element, "traverse, rels: #{rels.join(" ")}, revs: #{revs.join(" ")}") unless (rels + revs).empty? + add_debug(element) do + "rels: #{rels.join(" ")}, revs: #{revs.join(" ")}" + end unless (rels + revs).empty? if !(rel || rev) - # Establishing a new subject if no rel/rev [7.5 Step 6] + # Establishing a new subject if no rel/rev [7.5 Step 5] # May not be valid, but can exist new_subject = if about process_uri(element, about, evaluation_context, base, :uri_mappings => uri_mappings, :restrictions => SafeCURIEorCURIEorURI[@version]) @@ -775,13 +784,13 @@ else # if it's null, it's null and nothing changes skip = true unless property evaluation_context.parent_object end - add_debug(element, "[Step 6] new_subject: #{new_subject}, skip = #{skip}") + add_debug(element, "[Step 5] new_subject: #{new_subject}, skip = #{skip}") else - # [7.5 Step 7] + # [7.5 Step 6] # If the current element does contain a @rel or @rev attribute, then the next step is to # establish both a value for new subject and a value for current object resource: new_subject = process_uri(element, about, evaluation_context, base, :uri_mappings => uri_mappings, :restrictions => SafeCURIEorCURIEorURI[@version]) || @@ -813,14 +822,14 @@ elsif href process_uri(element, href, evaluation_context, base, :restrictions => [:uri]) end - add_debug(element, "[Step 7] new_subject: #{new_subject}, current_object_resource = #{current_object_resource.nil? ? 'nil' : current_object_resource}") + add_debug(element, "[Step 6] new_subject: #{new_subject}, current_object_resource = #{current_object_resource.nil? ? 'nil' : current_object_resource}") end - # Process @typeof if there is a subject [Step 8] + # Process @typeof if there is a subject [Step 7] if new_subject and typeof # Typeof is TERMorCURIEorAbsURIs types = process_uris(element, typeof, evaluation_context, base, :uri_mappings => uri_mappings, :term_mappings => term_mappings, @@ -829,35 +838,78 @@ add_debug(element, "typeof: #{typeof}") types.each do |one_type| add_triple(element, new_subject, RDF["type"], one_type) end end - - # Generate triples with given object [Step 9] + + # Collections: If new subject is set and is not the same as parent subject, + # replace the collection mappings taken from + # the evaluation context with a new empty mappings. + if (new_subject && new_subject != evaluation_context.parent_subject) || collection_mappings.nil? + collection_mappings = {} + add_debug(element) do + "collections: create new collection mappings(#{collection_mappings.object_id}) " + + "ns: #{new_subject}, " + + "ps: #{evaluation_context.parent_subject}" + end + end + + # Generate triples with given object [Step 8] + # + # Collections: if the current element has a @member attribute, add the property to the + # collection associated with that property, creating a new collection if necessary. if new_subject and current_object_resource rels.each do |r| - add_triple(element, new_subject, r, current_object_resource) + if member + # If the current collection mappings does not contain a collection associated with this IRI, + # instantiate a new collection + unless collection_mappings[r] + collection_mappings[r] = RDF::List.new + add_debug(element) {"collections(#{r}): create #{collection_mappings[r]}"} + end + add_debug(element, "member: add #{current_object_resource} to #{r} collection") + collection_mappings[r] << current_object_resource + else + add_triple(element, new_subject, r, current_object_resource) + end end revs.each do |r| add_triple(element, current_object_resource, r, new_subject) end elsif rel || rev - # Incomplete triples and bnode creation [Step 10] - add_debug(element, "[Step 10] incompletes: rels: #{rels}, revs: #{revs}") + # Incomplete triples and bnode creation [Step 9] + add_debug(element) {"[Step 9] incompletes: rels: #{rels}, revs: #{revs}"} current_object_resource = RDF::Node.new + # predicate: full IRI + # direction: forward/reverse + # collection: Save into collection, don't generate triple + rels.each do |r| - incomplete_triples << {:predicate => r, :direction => :forward} + if member + # If the current collection mappings does not contain a collection associated with this IRI, + # instantiate a new collection + unless collection_mappings[r] + collection_mappings[r] = RDF::List.new + add_debug(element) {"collections(#{r}): create #{collection_mappings[r]}"} + end + incomplete_triples << {:collection => collection_mappings[r]} + else + incomplete_triples << {:predicate => r, :direction => :forward} + end end revs.each do |r| incomplete_triples << {:predicate => r, :direction => :reverse} end end - # Establish current object literal [Step 11] + # Establish current object literal [Step 10] + # + # Collections: if the current element has a @member attribute, add the property to the + # collection associated with that property, creating a new collection if necessary. if property properties = process_uris(element, property, evaluation_context, base, :uri_mappings => uri_mappings, :term_mappings => term_mappings, :vocab => default_vocabulary, @@ -882,16 +934,16 @@ :vocab => default_vocabulary, :restrictions => TERMorCURIEorAbsURI[@version]) unless datatype.to_s.empty? begin current_object_literal = if !datatype.to_s.empty? && datatype.to_s != RDF.XMLLiteral.to_s # typed literal - add_debug(element, "[Step 11] typed literal (#{datatype})") + add_debug(element, "[Step 10] typed literal (#{datatype})") RDF::Literal.new(content || element.inner_text.to_s, :datatype => datatype, :language => language, :validate => validate?, :canonicalize => canonicalize?) elsif @version == :"rdfa1.1" if datatype.to_s == RDF.XMLLiteral.to_s # XML Literal - add_debug(element, "[Step 11(1.1)] XML Literal: #{element.inner_html}") + add_debug(element) {"[Step 10(1.1)] XML Literal: #{element.inner_html}"} # In order to maintain maximum portability of this literal, any children of the current node that are # elements must have the current in scope XML namespace declarations (if any) declared on the # serialized element using their respective attributes. Since the child element node could also # declare new XML namespaces, the RDFa Processor must be careful to merge these together when @@ -907,21 +959,21 @@ rescue ArgumentError => e add_error(element, e.message) end else # plain literal - add_debug(element, "[Step 11(1.1)] plain literal") + add_debug(element, "[Step 10(1.1)] plain literal") RDF::Literal.new(content || element.inner_text.to_s, :language => language, :validate => validate?, :canonicalize => canonicalize?) end else if content || (children_node_types == [Nokogiri::XML::Text]) || (element.children.length == 0) || datatype == "" # plain literal - add_debug(element, "[Step 11 (1.0)] plain literal") + add_debug(element, "[Step 10 (1.0)] plain literal") RDF::Literal.new(content || element.inner_text.to_s, :language => language, :validate => validate?, :canonicalize => canonicalize?) elsif children_node_types != [Nokogiri::XML::Text] and (datatype == nil or datatype.to_s == RDF.XMLLiteral.to_s) # XML Literal - add_debug(element, "[Step 11 (1.0)] XML Literal: #{element.inner_html}") + add_debug(element) {"[Step 10 (1.0)] XML Literal: #{element.inner_html}"} recurse = false RDF::Literal.new(element.inner_html, :datatype => RDF.XMLLiteral, :language => language, :namespaces => namespaces, @@ -933,45 +985,67 @@ add_error(element, e.message) end # add each property properties.each do |p| - add_triple(element, new_subject, p, current_object_literal) if new_subject + # Collections: If element has an @member attribute, add the value to a collection + if member + # If the current collection mappings does not contain a collection associated with this IRI, + # instantiate a new collection + unless collection_mappings[p] + collection_mappings[p] = RDF::List.new + add_debug(element) {"collections(#{p}): create #{collection_mappings[p]}"} + end + add_debug(element) {"member: add #{current_object_literal} to #{p} collection"} + collection_mappings[p] << current_object_literal + elsif new_subject + add_triple(element, new_subject, p, current_object_literal) + end end end if not skip and new_subject && !evaluation_context.incomplete_triples.empty? - # Complete the incomplete triples from the evaluation context [Step 12] - add_debug(element, "[Step 12] complete incomplete triples: new_subject=#{new_subject}, completes=#{evaluation_context.incomplete_triples.inspect}") + # Complete the incomplete triples from the evaluation context [Step 11] + add_debug(element) do + "[Step 11] complete incomplete triples: " + + "new_subject=#{new_subject.inspect}, " + + "completes=#{evaluation_context.incomplete_triples.inspect}" + end + evaluation_context.incomplete_triples.each do |trip| - if trip[:direction] == :forward + if trip[:collection] + add_debug(element) {"member: add #{current_object_resource} to #{trip[:collection]} collection"} + trip[:collection] << new_subject + elsif trip[:direction] == :forward add_triple(element, evaluation_context.parent_subject, trip[:predicate], new_subject) elsif trip[:direction] == :reverse add_triple(element, new_subject, trip[:predicate], evaluation_context.parent_subject) end end end - # Create a new evaluation context and proceed recursively [Step 13] + # Create a new evaluation context and proceed recursively [Step 12] if recurse if skip if language == evaluation_context.language && uri_mappings == evaluation_context.uri_mappings && term_mappings == evaluation_context.term_mappings && default_vocabulary == evaluation_context.default_vocabulary && - base == evaluation_context.base + base == evaluation_context.base && + collection_mappings == evaluation_context.collection_mappings new_ec = evaluation_context - add_debug(element, "[Step 13] skip: reused ec") + add_debug(element, "[Step 12] skip: reused ec") else new_ec = evaluation_context.clone new_ec.base = base new_ec.language = language new_ec.uri_mappings = uri_mappings new_ec.namespaces = namespaces new_ec.term_mappings = term_mappings new_ec.default_vocabulary = default_vocabulary - add_debug(element, "[Step 13] skip: cloned ec") + new_ec.collection_mappings = collection_mappings + add_debug(element, "[Step 12] skip: cloned ec") end else # create a new evaluation context new_ec = EvaluationContext.new(base, @host_defaults) new_ec.parent_subject = new_subject || evaluation_context.parent_subject @@ -980,54 +1054,78 @@ new_ec.namespaces = namespaces new_ec.incomplete_triples = incomplete_triples new_ec.language = language new_ec.term_mappings = term_mappings new_ec.default_vocabulary = default_vocabulary - add_debug(element, "[Step 13] new ec") + new_ec.collection_mappings = collection_mappings + add_debug(element, "[Step 12] new ec") end element.children.each do |child| # recurse only if it's an element traverse(child, new_ec) if child.class == Nokogiri::XML::Element end + + # Collections: after traversing through child elements, for each collection associated with + # a property + collection_mappings.each do |p, c| + # if that collection is different from the evaluation context + ec_col = evaluation_context.collection_mappings[p] if evaluation_context.collection_mappings + add_debug(element) {"collections: time to create #{c}? #{(ec_col != c).inspect}"} + if ec_col != c + add_debug(element) {"collection(#{p}) create #{c}"} + # Generate an rdf:List with the elements of that collection. + c.each_statement do |st| + add_triple(element, st.subject, st.predicate, st.object) unless st.object == RDF.List + end + + # Generate a triple relating new_subject, property and the collection BNode, + # or rdf:nil if the collection is empty. + if c.empty? + add_triple(element, new_subject, p, RDF.nil) + else + add_triple(element, new_subject, p, c.subject) + end + end + end end end # space-separated TERMorCURIEorAbsURI or SafeCURIEorCURIEorURI def process_uris(element, value, evaluation_context, base, options) return [] if value.to_s.empty? - add_debug(element, "process_uris: #{value}") + add_debug(element) {"process_uris: #{value}"} value.to_s.split(/\s+/).map {|v| process_uri(element, v, evaluation_context, base, options)}.compact end def process_uri(element, value, evaluation_context, base, options = {}) return if value.nil? restrictions = options[:restrictions] - add_debug(element, "process_uri: #{value}, restrictions = #{restrictions.inspect}") + add_debug(element) {"process_uri: #{value}, restrictions = #{restrictions.inspect}"} options = {:uri_mappings => {}}.merge(options) if !options[:term_mappings] && options[:uri_mappings] && value.to_s.match(/^\[(.*)\]$/) && restrictions.include?(:safe_curie) # SafeCURIEorCURIEorURI # When the value is surrounded by square brackets, then the content within the brackets is # evaluated as a CURIE according to the CURIE Syntax definition. If it is not a valid CURIE, the # value must be ignored. uri = curie_to_resource_or_bnode(element, $1, options[:uri_mappings], evaluation_context.parent_subject, restrictions) - add_debug(element, "process_uri: #{value} => safeCURIE => <#{uri}>") + add_debug(element) {"process_uri: #{value} => safeCURIE => <#{uri}>"} uri elsif options[:term_mappings] && NC_REGEXP.match(value.to_s) && restrictions.include?(:term) # TERMorCURIEorAbsURI # If the value is an NCName, then it is evaluated as a term according to General Use of Terms in # Attributes. Note that this step may mean that the value is to be ignored. uri = process_term(element, value.to_s, options) - add_debug(element, "process_uri: #{value} => term => <#{uri}>") + add_debug(element) {"process_uri: #{value} => term => <#{uri}>"} uri else # SafeCURIEorCURIEorURI or TERMorCURIEorAbsURI # Otherwise, the value is evaluated as a CURIE. # If it is a valid CURIE, the resulting URI is used; otherwise, the value will be processed as a URI. uri = curie_to_resource_or_bnode(element, value, options[:uri_mappings], evaluation_context.parent_subject, restrictions) if uri - add_debug(element, "process_uri: #{value} => CURIE => <#{uri}>") + add_debug(element) {"process_uri: #{value} => CURIE => <#{uri}>"} elsif @version == :"rdfa1.0" && value.to_s.match(/^xml/i) # Special case to not allow anything starting with XML to be treated as a URI elsif restrictions.include?(:absuri) || restrictions.include?(:uri) begin # AbsURI does not use xml:base @@ -1048,22 +1146,17 @@ add_warning(element, "Undefined prefix #{$1}", RDF::RDFA.UnresolvedCURIE) else add_warning(element, "Relative URI #{value}") end end - add_debug(element, "process_uri: #{value} => URI => <#{uri}>") + add_debug(element) {"process_uri: #{value} => URI => <#{uri}>"} end uri end end # [7.4.3] General Use of Terms in Attributes - # - # @param [String] term:: term - # @param [Hash] options:: Parser options, one of - # <em>options[:term_mappings]</em>:: Term mappings - # <em>options[:vocab]</em>:: Default vocabulary def process_term(element, value, options) if options[:term_mappings].is_a?(Hash) # If the term is in the local term mappings, use the associated URI (case sensitive). return uri(options[:term_mappings][value.to_s.to_sym]) if options[:term_mappings].has_key?(value.to_s.to_sym) @@ -1100,16 +1193,18 @@ # No prefix, undefined (in this context, it is evaluated as a term elsewhere) nil else # Prefixes always downcased prefix = prefix.to_s.downcase unless @version == :"rdfa1.0" - add_debug(element, "curie_to_resource_or_bnode check for #{prefix.to_s.to_sym.inspect} in #{uri_mappings.inspect}") + add_debug(element) do + "curie_to_resource_or_bnode check for #{prefix.to_s.to_sym.inspect} in #{uri_mappings.inspect}" + end ns = uri_mappings[prefix.to_s.to_sym] if ns uri(ns + reference.to_s) else - add_debug(element, "curie_to_resource_or_bnode No namespace mapping for #{prefix}") + add_debug(element) {"curie_to_resource_or_bnode No namespace mapping for #{prefix}"} nil end end end @@ -1120,6 +1215,6 @@ value.canonicalize! if canonicalize? value = RDF::URI.intern(value) if intern? value end end -end \ No newline at end of file +end