text:
# frozen_string_literal: true

require 'json/ld'
require 'json/ld/expand'
require 'json/ld/to_rdf'

module JSON
  module LD
    ##
    # A streaming JSON-LD parser in Ruby.
    #
    # @see http://json-ld.org/spec/ED/20110507/
    # @author [Gregg Kellogg](http://greggkellogg.net/)
    module StreamingReader
      include Utils
      include JSON::LD::ToRDF # For value object conversion

      # The base URI to use when resolving relative URIs
      # @return [RDF::URI]
      attr_reader :base
      attr_reader :namer

      def self.format
        JSON::LD::Format
      end

      ##
      # @see   RDF::Reader#each_statement
      def stream_statement
        unique_bnodes = @options[:unique_bnodes]
        rename_bnodes = @options.fetch(:rename_bnodes, true)
        # FIXME: document loader doesn't stream
        @base = RDF::URI(@options[:base] || base_uri)
        mj_opts = @options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
        value = MultiJson.load(@doc, mj_opts)
        context_ref = @options[:expandContext]
        # context_ref = @options.fetch(:expandContext, remote_doc.contextUrl)
        context = Context.parse(context_ref, **@options)

        @namer = if unique_bnodes
          BlankNodeUniqer.new
        else
          (rename_bnodes ? BlankNodeNamer.new("b") : BlankNodeMapper.new)
        end
        # Namer for naming provisional nodes, which may be determined later to be actual
        @provisional_namer = BlankNodeNamer.new("p")

        parse_object(value, nil, context, graph_is_named: false) do |st|
          # Only output reasonably valid triples
          yield(st) if st.to_a.all? { |r| r.is_a?(RDF::Term) && (r.uri? ? r.valid? : true) }
        end
      rescue ::JSON::ParserError, ::JSON::LD::JsonLdError => e
        log_fatal("Failed to parse input document: #{e.message}", exception: RDF::ReaderError)
      end

      private

      # Parse a node object, or array of node objects
      #
      # @param [Array, Hash] input
      # @param [String] active_property
      #   The unexpanded property referencing this object
      # @param [Context] context
      # @param [RDF::Resource] subject referencing this object
      # @param [RDF::URI] predicate the predicate part of the reference
      # @param [Boolean] from_map
      #   Expanding from a map, which could be an `@type` map, so don't clear out context term definitions
      # @param [Boolean] graph_is_named
      #   Use of `@graph` implies a named graph; not true at the top-level.
      # @param [RDF::URI] extra_type from a type map
      # @param [String] language from a language map
      # @param [RDF::Resource] node_id from an id map
      # @return [void]
      def parse_object(input, active_property, context,
                       subject: nil, predicate: nil, from_map: false,
                       extra_type: nil, language: nil, node_id: nil,
                       graph_is_named: true, &block)

        # Skip predicates that look like a BNode
        if predicate.to_s.start_with?('_:')
          warn "[DEPRECATION] Blank Node properties deprecated in JSON-LD 1.1."
          return
        end

        if input.is_a?(Array)
          input.each do |e|
            parse_object(e, active_property, context, subject: subject, predicate: predicate, from_map: from_map,
              &block)
          end
          return
        end

        # Note that we haven't parsed an @id key, so have no subject
        have_id = false
        node_reference = false
        is_list_or_set = false
        node_id ||= RDF::Node.new(@provisional_namer.get_sym)
        # For keeping statements not yet ready to be emitted
        provisional_statements = []
        value_object = {}

        # Use a term-specific context, if defined, based on the non-type-scoped context.
        if active_property && context.term_definitions[active_property]
          property_scoped_context = context.term_definitions[active_property].context
        end

        # Revert any previously type-scoped term definitions, unless this is from a map, a value object or a subject reference
        # FIXME
        if input.is_a?(Hash) && context.previous_context
          expanded_key_map = input.keys.inject({}) do |memo, key|
            memo.merge(key => context.expand_iri(key, vocab: true, as_string: true, base: base))
          end
          revert_context = !from_map &&
                           !expanded_key_map.value?('@value') &&
                           expanded_key_map.values != ['@id']
          context = context.previous_context if revert_context
        end

        # Apply property-scoped context after reverting term-scoped context
        context = context.parse(property_scoped_context, base: base, override_protected: true) unless
          property_scoped_context.nil?

        # Otherwise, unless the value is a number, expand the value according to the Value Expansion rules, passing active property.
        input = context.expand_value(active_property, input, base: base) unless input.is_a?(Hash)

        # Output any type provided from a type map
        provisional_statements << RDF::Statement(node_id, RDF.type, extra_type) if
          extra_type

        # Add statement, either provisionally, or just emit
        add_statement = proc do |st|
          if have_id || st.to_quad.none?(node_id)
            yield(st)
          else
            provisional_statements << st
          end
        end

        # Input is an object (Hash), parse keys in order
        state = :await_context
        input.each do |key, value|
          expanded_key = context.expand_iri(key, base: base, vocab: true)
          case expanded_key
          when '@context'
            unless state == :await_context
              raise JsonLdError::InvalidStreamingKeyOrder,
                "found #{key} in state #{state}"
            end
            context = context.parse(value, base: base)
            state = :await_type
          when '@type'
            # Set the type-scoped context to the context on input, for use later
            unless %i[await_context await_type].include?(state)
              raise JsonLdError::InvalidStreamingKeyOrder,
                "found #{key} in state #{state}"
            end

            type_scoped_context = context
            as_array(value).sort.each do |term|
              unless term.is_a?(String)
                raise JsonLdError::InvalidTypeValue,
                  "value of @type must be a string: #{term.inspect}"
              end
              if type_scoped_context.term_definitions[term]
                term_context = type_scoped_context.term_definitions[term].context
              end
              context = context.parse(term_context, base: base, propagate: false) unless term_context.nil?
              type = type_scoped_context.expand_iri(term,
                base: base,
                documentRelative: true,
                vocab: true)

              # Early terminate for @json
              type = RDF.JSON if type == '@json'
              # Add a provisional statement
              provisional_statements << RDF::Statement(node_id, RDF.type, type)
            end
            state = :await_type
          when '@id'
            if is_list_or_set
              raise JsonLdError::InvalidSetOrListObject,
                "found #{key} in state #{state}"
            end
            unless %i[await_context await_type await_id].include?(state)
              raise JsonLdError::CollidingKeywords,
                "found #{key} in state #{state}"
            end

            # Set our actual id, and use for replacing any provisional statements using our existing node_id, which is provisional
            unless value.is_a?(String)
              raise JsonLdError::InvalidIdValue,
                "value of @id must be a string: #{value.inspect}"
            end
            node_reference = input.keys.length == 1
            expanded_id = context.expand_iri(value, base: base, documentRelative: true)
            next if expanded_id.nil?

            new_node_id = as_resource(expanded_id)
            # Replace and emit any statements including our provisional id with the newly established node (or graph) id
            provisional_statements.each do |st|
              st.subject = new_node_id if st.subject == node_id
              st.object = new_node_id if st.object == node_id
              st.graph_name = new_node_id if st.graph_name == node_id
              yield(st)
            end

            provisional_statements.clear
            have_id = true
            node_id = new_node_id

            # if there's a subject & predicate, emit that statement now
            if subject && predicate
              st = RDF::Statement(subject, predicate, node_id)
              yield(st)
            end
            state = :properties

          when '@direction'
            if state == :properties
              raise JsonLdError::InvalidStreamingKeyOrder,
                "found @direction in state #{state}"
            end
            value_object['@direction'] = value
            state = :await_id
          when '@graph'
            # If `@graph` is at the top level (no `subject`) and value contains no keys other than `@graph` and `@context`, add triples to the default graph
            # Process all graph statements
            parse_object(value, nil, context) do |st|
              # If `@graph` is at the top level (`graph_is_named` is `false`) and input contains no keys other than `@graph` and `@context`, add triples to the default graph
              relevant_keys = input.keys - ['@context', key]
              st.graph_name = node_id unless !graph_is_named && relevant_keys.empty?
              if st.graph_name && !st.graph_name.valid?
                warn "skipping graph statement within invalid graph name: #{st.inspect}"
              else
                add_statement.call(st)
              end
            end
            state = :await_id unless state == :properties
          when '@included'
            # Expanded values must be node objects
            have_statements = false
            parse_object(value, active_property, context) do |st|
              have_statements ||= st.subject?
              yield(st)
            end
            unless have_statements
              raise JsonLdError::InvalidIncludedValue,
                "values of @included must expand to node objects"
            end

            state = :await_id unless state == :properties
          when '@index'
            state = :await_id unless state == :properties
            unless value.is_a?(String)
              raise JsonLdError::InvalidIndexValue,
                "Value of @index is not a string: #{value.inspect}"
            end
          when '@language'
            if state == :properties
              raise JsonLdError::InvalidStreamingKeyOrder,
                "found @language in state #{state}"
            end
            unless value.is_a?(String)
              raise JsonLdError::InvalidLanguageTaggedString,
                "@language value must be a string: #{value.inspect}"
            end
            unless /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(value)
              warn "@language must be valid BCP47: #{value.inspect}"
              return
            end
            language = value
            state = :await_id
          when '@list'
            unless %i[await_context await_type await_id].include?(state)
              raise JsonLdError::InvalidSetOrListObject,
                "found #{key} in state #{state}"
            end
            is_list_or_set = true
            node_id = parse_list(value, active_property, context, &block) if subject
            state = :properties
          when '@nest'
            if context.term_definitions[active_property]
              nest_context = context.term_definitions[active_property].context
            end
            nest_context = if nest_context.nil?
              context
            else
              context.parse(nest_context, base: base, override_protected: true)
            end
            as_array(value).each do |v|
              raise JsonLdError::InvalidNestValue, v.inspect unless
                v.is_a?(Hash) && v.keys.none? { |k| nest_context.expand_iri(k, vocab: true, base: base) == '@value' }

              parse_object(v, active_property, nest_context, node_id: node_id) do |st|
                add_statement.call(st)
              end
            end
            state = :await_id unless state == :properties
          when '@reverse'
            as_array(value).each do |item|
              item = context.expand_value(active_property, item, base: base) unless item.is_a?(Hash)
              raise JsonLdError::InvalidReverseValue, item.inspect if value?(item)
              raise JsonLdError::InvalidReversePropertyMap, item.inspect if node_reference?(item)
              raise JsonLdError::InvalidReversePropertyValue, item.inspect if list?(item)

              has_own_subject = false
              parse_object(item, active_property, context, node_id: node_id, predicate: predicate) do |st|
                if st.subject == node_id
                  raise JsonLdError::InvalidReversePropertyValue, item.inspect unless st.object.resource?

                  # Invert sense of statements
                  st = RDF::Statement(st.object, st.predicate, st.subject)
                  has_own_subject = true
                end
                add_statement.call(st)
              end

              # If the reversed node does not make any claims on this subject, it's an error
              raise JsonLdError::InvalidReversePropertyValue, item.inspect unless has_own_subject
            end
            state = :await_id unless state == :properties
          when '@set'
            unless %i[await_context await_type await_id].include?(state)
              raise JsonLdError::InvalidSetOrListObject,
                "found #{key} in state #{state}"
            end
            is_list_or_set = true
            value = as_array(value).compact
            parse_object(value, active_property, context, subject: subject, predicate: predicate, &block)
            node_id = nil
            state = :properties
          when '@value'
            if state == :properties
              raise JsonLdError::InvalidStreamingKeyOrder,
                "found @value in state #{state}"
            end
            value_object['@value'] = value
            state = :await_id
          else
            state = :await_id unless state == :properties
            # Skip keys that don't expand to a keyword or absolute IRI
            next if expanded_key.is_a?(RDF::URI) && !expanded_key.absolute?

            parse_property(value, key, context, node_id, expanded_key) do |st|
              add_statement.call(st)
            end
          end
        end

        # Value object with @id
        if !value_object.empty? && (have_id || is_list_or_set)
          raise JsonLdError::InvalidValueObject,
            "value object has unknown key: @id"
        end

        # Can't have both @id and either @list or @set
        if have_id && is_list_or_set
          raise JsonLdError::InvalidSetOrListObject,
            "found @id with @list or @set"
        end

        type_statements = provisional_statements.select { |ps| ps.predicate == RDF.type && ps.graph_name.nil? }
        value_object['@language'] = (@options[:lowercaseLanguage] ? language.downcase : language) if language
        if !value_object.empty? &&
           (!value_object['@value'].nil? ||
            (type_statements.first || RDF::Statement.new).object == RDF.JSON)

          # There can be only one value of @type
          case type_statements.length
          when 0 # skip
          when 1
            unless type_statements.first.object.valid?
              raise JsonLdError::InvalidTypedValue,
                "value of @type must be an IRI or '@json': #{type_statements.first.object.inspect}"
            end
            value_object['@type'] = type_statements.first.object
          else
            raise JsonLdError::InvalidValueObject,
              "value object must not have more than one type"
          end

          # Check for extra keys
          unless (value_object.keys - Expand::KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION).empty?
            raise JsonLdError::InvalidValueObject,
              "value object has unknown keys: #{value_object.inspect}"
          end

          # @type is inconsistent with either @language or @direction
          if value_object.key?('@type') && !(value_object.keys & %w[@language @direction]).empty?
            raise JsonLdError::InvalidValueObject,
              "value object must not include @type with either " \
              "@language or @direction: #{value_object.inspect}"
          end

          if value_object.key?('@language') && !value_object['@value'].is_a?(String)
            raise JsonLdError::InvalidLanguageTaggedValue,
              "with @language @value must be a string: #{value_object.inspect}"
          elsif value_object['@type'] && value_object['@type'] != RDF.JSON
            unless value_object['@type'].is_a?(RDF::URI)
              raise JsonLdError::InvalidTypedValue,
                "value of @type must be an IRI or '@json': #{value_object['@type'].inspect}"
            end
          elsif value_object['@type'] != RDF.JSON
            case value_object['@value']
            when String, TrueClass, FalseClass, Numeric # okay
            else
              raise JsonLdError::InvalidValueObjectValue,
                "@value is: #{value_object['@value'].inspect}"
            end
          end
          literal = item_to_rdf(value_object, &block)
          st = RDF::Statement(subject, predicate, literal)
          yield(st)
        elsif !provisional_statements.empty?
          # Emit all provisional statements, as no @id was ever found
          provisional_statements.each(&block)
        end

        # Use implicit subject to generate the relationship
        return unless value_object.empty? && subject && predicate && !have_id && !node_reference

        yield(RDF::Statement(subject, predicate, node_id))
      end

      def parse_property(input, active_property, context, subject, predicate, &block)
        container = context.container(active_property)
        if container.include?('@language') && input.is_a?(Hash)
          input.each do |lang, lang_value|
            expanded_lang = context.expand_iri(lang, vocab: true)
            if lang !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/ && expanded_lang != '@none'
              warn "@language must be valid BCP47: #{lang.inspect}"
            end

            as_array(lang_value).each do |item|
              unless item.nil? || item.is_a?(String)
                raise JsonLdError::InvalidLanguageMapValue,
                  "Expected #{item.inspect} to be a string"
              end
              lang_obj = { '@value' => item }
              lang_obj['@language'] = lang unless expanded_lang == '@none'
              lang_obj['@direction'] = context.direction(lang) if context.direction(lang)
              parse_object(lang_obj, active_property, context, subject: subject, predicate: predicate, &block)
            end
          end
        elsif container.include?('@list')
          # Handle case where value is a list object
          if input.is_a?(Hash) &&
             input.keys.map do |k|
               context.expand_iri(k, vocab: true, as_string: true, base: base)
             end.include?('@list')
            parse_object(input, active_property, context,
              subject: subject, predicate: predicate, &block)
          else
            list = parse_list(input, active_property, context, &block)
            yield(RDF::Statement(subject, predicate, list))
          end
        elsif container.intersect?(JSON::LD::Expand::CONTAINER_INDEX_ID_TYPE) && input.is_a?(Hash)
          # Get appropriate context for this container
          container_context = if container.include?('@type') && context.previous_context
            context.previous_context
          elsif container.include?('@id') && context.term_definitions[active_property]
            id_context = context.term_definitions[active_property].context if context.term_definitions[active_property]
            if id_context.nil?
              context
            else
              context.parse(id_context, base: base, propagate: false)
            end
          else
            context
          end

          input.each do |k, v|
            # If container mapping in the active context includes @type, and k is a term in the active context having a local context, use that context when expanding values
            map_context = container_context.term_definitions[k].context if
              container.include?('@type') && container_context.term_definitions[k]
            map_context = container_context.parse(map_context, base: base, propagate: false) unless map_context.nil?
            map_context ||= container_context

            expanded_k = container_context.expand_iri(k, vocab: true, as_string: true, base: base)
            index_key = context.term_definitions[active_property].index || '@index'

            if container.include?('@index') && container.include?('@graph')
              # Index is ignored
              as_array(v).each do |item|
                # Each value is in a separate graph
                graph_name = RDF::Node.new(namer.get_sym)
                parse_object(item, active_property, context) do |st|
                  st.graph_name ||= graph_name
                  yield(st)
                end
                yield(RDF::Statement(subject, predicate, graph_name))

                # Add a property index, if appropriate
                next if index_key == '@index'

                # Expand key based on term
                expanded_k = if k == '@none'
                  '@none'
                else
                  container_context.expand_value(index_key, k, base: base)
                end

                # Add the index property as a property of the graph name
                index_property = container_context.expand_iri(index_key, vocab: true, base: base)
                unless expanded_k == '@none'
                  emit_object(expanded_k, index_key, map_context, graph_name,
                    index_property, from_map: true, &block)
                end
              end
            elsif container.include?('@index')
              if index_key == '@index'
                # Index is ignored
                emit_object(v, active_property, map_context, subject, predicate, from_map: true, &block)
              else
                # Expand key based on term
                expanded_k = if k == '@none'
                  '@none'
                else
                  container_context.expand_value(index_key, k, base: base)
                end

                index_property = container_context.expand_iri(index_key, vocab: true, as_string: true, base: base)

                # index_key is a property
                as_array(v).each do |item|
                  item = container_context.expand_value(active_property, item, base: base) if item.is_a?(String)
                  if value?(item)
                    raise JsonLdError::InvalidValueObject,
                      "Attempt to add illegal key to value object: #{index_key}"
                  end
                  # add expanded_k as value of index_property in item
                  item[index_property] = [expanded_k].concat(Array(item[index_property])) unless expanded_k == '@none'
                  emit_object(item, active_property, map_context, subject, predicate, from_map: true, &block)
                end
              end
            elsif container.include?('@id') && container.include?('@graph')
              graph_name = if expanded_k == '@none'
                RDF::Node.new(namer.get_sym)
              else
                container_context.expand_iri(k, documentRelative: true, base: base)
              end
              parse_object(v, active_property, context) do |st|
                st.graph_name ||= graph_name
                yield(st)
              end
              yield(RDF::Statement(subject, predicate, graph_name))
            elsif container.include?('@id')
              expanded_k = container_context.expand_iri(k, documentRelative: true, base: base)
              # pass our id
              emit_object(v, active_property, map_context, subject, predicate,
                node_id: (expanded_k unless expanded_k == '@none'),
                from_map: true,
                &block)
            elsif container.include?('@type')
              emit_object(v, active_property, map_context, subject, predicate,
                from_map: true,
                extra_type: as_resource(expanded_k),
                &block)
            end
          end
        elsif container.include?('@graph')
          # Index is ignored
          as_array(input).each do |v|
            # Each value is in a separate graph
            graph_name = RDF::Node.new(namer.get_sym)
            parse_object(v, active_property, context) do |st|
              st.graph_name ||= graph_name
              yield(st)
            end
            yield(RDF::Statement(subject, predicate, graph_name))
          end
        else
          emit_object(input, active_property, context, subject, predicate, &block)
        end
      end

      # Wrapps parse_object to handle JSON literals and reversed properties
      def emit_object(input, active_property, context, subject, predicate, **options, &block)
        if context.coerce(active_property) == '@json'
          parse_object(context.expand_value(active_property, input), active_property, context,
            subject: subject, predicate: predicate, **options, &block)
        elsif context.reverse?(active_property)
          as_array(input).each do |item|
            item = context.expand_value(active_property, item, base: base) unless item.is_a?(Hash)
            raise JsonLdError::InvalidReverseValue, item.inspect if value?(item)
            raise JsonLdError::InvalidReversePropertyValue, item.inspect if list?(item)

            has_own_subject = false
            parse_object(item, active_property, context, subject: subject, predicate: predicate, **options) do |st|
              if st.subject == subject
                raise JsonLdError::InvalidReversePropertyValue, item.inspect unless st.object.resource?

                # Invert sense of statements
                st = RDF::Statement(st.object, st.predicate, st.subject)
                has_own_subject = true
              end
              yield(st)
            end

            # If the reversed node does not make any claims on this subject, it's an error
            unless has_own_subject
              raise JsonLdError::InvalidReversePropertyValue,
                "@reverse value must be a node: #{value.inspect}"
            end
          end
        else
          as_array(input).flatten.each do |item|
            # emit property/value
            parse_object(item, active_property, context,
              subject: subject, predicate: predicate, **options, &block)
          end
        end
      end

      # Process input as an ordered list
      # @return [RDF::Resource] the list head
      def parse_list(input, active_property, context, &block)
        # Transform all entries into their values
        # this allows us to eliminate those that don't create any statements
        fake_subject = RDF::Node.new
        values = as_array(input).map do |entry|
          if entry.is_a?(Array)
            # recursive list
            entry_value = parse_list(entry, active_property, context, &block)
          else
            entry_value = nil
            parse_object(entry, active_property, context, subject: fake_subject, predicate: RDF.first) do |st|
              if st.subject == fake_subject
                entry_value = st.object
              else
                yield(st)
              end
            end
            entry_value
          end
        end.compact
        return RDF.nil if values.empty?

        # Construct a list from values, and emit list statements, returning the list subject
        list = RDF::List(*values)
        list.each_statement(&block)
        list.subject
      end
    end
  end
end


text:
# frozen_string_literal: true

module JSON
  module LD
    module VERSION
      VERSION_FILE = File.join(__dir__, "..", "..", "..", "VERSION")
      MAJOR, MINOR, TINY, EXTRA = File.read(VERSION_FILE).chomp.split(".")

      STRING = [MAJOR, MINOR, TINY, EXTRA].compact.join('.')

      ##
      # @return [String]
      def self.to_s
        STRING
      end

      ##
      # @return [String]
      def self.to_str
        STRING
      end

      ##
      # @return [Array(Integer, Integer, Integer)]
      def self.to_a
        STRING.split(".")
      end
    end
  end
end

repo_name: ruby-rdf/json-ld

repo_language: HTML

mime_type: text/plain


text:
# frozen_string_literal: true

require 'rdf'
require 'rdf/nquads'
require 'json/canonicalization'

module JSON
  module LD
    module ToRDF
      include Utils

      ##
      # @param [Hash{String => Object}] item
      # @param [RDF::Resource] graph_name
      # @param [Boolean] quoted emitted triples are quoted triples.
      # @yield statement
      # @yieldparam [RDF::Statement] statement
      # @return RDF::Resource the subject of this item
      def item_to_rdf(item, graph_name: nil, quoted: false, &block)
        # Just return value object as Term
        return unless item

        if value?(item)
          value = item.fetch('@value')
          datatype = item.fetch('@type', nil)

          datatype = RDF_JSON if datatype == '@json'

          case value
          when RDF::Value
            return value
          when TrueClass, FalseClass
            # If value is true or false, then set value its canonical lexical form as defined in the section Data Round Tripping. If datatype is null, set it to xsd:boolean.
            value = value.to_s
            datatype ||= RDF::XSD.boolean.to_s
          when Numeric
            # Otherwise, if value is a number, then set value to its canonical lexical form as defined in the section Data Round Tripping. If datatype is null, set it to either xsd:integer or xsd:double, depending on if the value contains a fractional and/or an exponential component.
            value = if datatype == RDF_JSON
              value.to_json_c14n
            else
              # Don't serialize as double if there are no fractional bits
              as_double = value.ceil != value || value >= 1e21 || datatype == RDF::XSD.double
              lit = if as_double
                RDF::Literal::Double.new(value, canonicalize: true)
              else
                RDF::Literal.new(value.numerator, canonicalize: true)
              end

              datatype ||= lit.datatype
              lit.to_s.sub("E+", "E")
            end
          when Array, Hash
            # Only valid for rdf:JSON
            value = value.to_json_c14n
          else
            if item.key?('@direction') && @options[:rdfDirection]
              # Either serialize using a datatype, or a compound-literal
              case @options[:rdfDirection]
              when 'i18n-datatype'
                datatype = RDF::URI("https://www.w3.org/ns/i18n##{item.fetch('@language',
                  '').downcase}_#{item['@direction']}")
              when 'compound-literal'
                cl = RDF::Node.new
                yield RDF::Statement(cl, RDF.value, item['@value'].to_s)
                yield RDF::Statement(cl, RDF_LANGUAGE, item['@language'].downcase) if item['@language']
                yield RDF::Statement(cl, RDF_DIRECTION, item['@direction'])
                return cl
              end
            end

            # Otherwise, if datatype is null, set it to xsd:string or xsd:langString, depending on if item has a @language key.
            datatype ||= item.key?('@language') ? RDF.langString : RDF::XSD.string
            value = value.to_json_c14n if datatype == RDF_JSON
          end
          datatype = RDF::URI(datatype) if datatype && !datatype.is_a?(RDF::URI)

          # Initialize literal as an RDF literal using value and datatype. If element has the key @language and datatype is xsd:string, then add the value associated with the @language key as the language of the object.
          language = item.fetch('@language', nil) if datatype == RDF.langString
          return RDF::Literal.new(value, datatype: datatype, language: language)
        elsif list?(item)
          # If item is a list object, initialize list_results as an empty array, and object to the result of the List Conversion algorithm, passing the value associated with the @list key from item and list_results.
          return parse_list(item['@list'], graph_name: graph_name, &block)
        end

        subject = case item['@id']
        when nil then node
        when String then as_resource(item['@id'])
        when Object
          # Embedded/quoted statement
          # (No error checking, as this is done in expansion)
          to_enum(:item_to_rdf, item['@id'], quoted: true).to_a.first
        end

        # log_debug("item_to_rdf")  {"subject: #{subject.to_ntriples rescue 'malformed rdf'}"}
        item.each do |property, values|
          case property
          when '@type'
            # If property is @type, construct triple as an RDF Triple composed of id, rdf:type, and object from values where id and object are represented either as IRIs or Blank Nodes
            values.each do |v|
              object = as_resource(v)
              # log_debug("item_to_rdf")  {"type: #{object.to_ntriples rescue 'malformed rdf'}"}
              yield RDF::Statement(subject, RDF.type, object, graph_name: graph_name, quoted: quoted)
            end
          when '@graph'
            values = [values].compact unless values.is_a?(Array)
            values.each do |nd|
              item_to_rdf(nd, graph_name: subject, quoted: quoted, &block)
            end
          when '@reverse'
            raise "Huh?" unless values.is_a?(Hash)

            values.each do |prop, vv|
              predicate = as_resource(prop)
              # log_debug("item_to_rdf")  {"@reverse predicate: #{predicate.to_ntriples rescue 'malformed rdf'}"}
              # For each item in values
              vv.each do |v|
                # Item is a node definition. Generate object as the result of the Object Converstion algorithm passing item.
                object = item_to_rdf(v, graph_name: graph_name, &block)
                # log_debug("item_to_rdf")  {"subject: #{object.to_ntriples rescue 'malformed rdf'}"}
                # yield subject, prediate, and literal to results.
                yield RDF::Statement(object, predicate, subject, graph_name: graph_name, quoted: quoted)
              end
            end
          when '@included'
            values.each do |v|
              item_to_rdf(v, graph_name: graph_name, &block)
            end
          when /^@/
            # Otherwise, if @type is any other keyword, skip to the next property-values pair
          else
            # Otherwise, property is an IRI or Blank Node identifier
            # Initialize predicate from  property as an IRI or Blank node
            predicate = as_resource(property)
            # log_debug("item_to_rdf")  {"predicate: #{predicate.to_ntriples rescue 'malformed rdf'}"}

            # For each item in values
            values.each do |v|
              if list?(v)
                # log_debug("item_to_rdf")  {"list: #{v.inspect}"}
                # If item is a list object, initialize list_results as an empty array, and object to the result of the List Conversion algorithm, passing the value associated with the @list key from item and list_results.
                object = parse_list(v['@list'], graph_name: graph_name, &block)

                # Append a triple composed of subject, prediate, and object to results and add all triples from list_results to results.
              else
                # Otherwise, item is a value object or a node definition. Generate object as the result of the Object Converstion algorithm passing item.
                object = item_to_rdf(v, graph_name: graph_name, &block)
                # log_debug("item_to_rdf")  {"object: #{object.to_ntriples rescue 'malformed rdf'}"}
                # yield subject, prediate, and literal to results.
              end
              yield RDF::Statement(subject, predicate, object, graph_name: graph_name, quoted: quoted)
            end
          end
        end

        subject
      end

      ##
      # Parse a List
      #
      # @param [Array] list
      #   The Array to serialize as a list
      # @yield statement
      # @yieldparam [RDF::Resource] statement
      # @return [Array<RDF::Statement>]
      #   Statements for each item in the list
      def parse_list(list, graph_name: nil, &block)
        # log_debug('parse_list') {"list: #{list.inspect}"}

        last = list.pop
        result = first_bnode = last ? node : RDF.nil

        list.each do |list_item|
          # Set first to the result of the Object Converstion algorithm passing item.
          object = item_to_rdf(list_item, graph_name: graph_name, &block)
          yield RDF::Statement(first_bnode, RDF.first, object, graph_name: graph_name)
          rest_bnode = node
          yield RDF::Statement(first_bnode, RDF.rest, rest_bnode, graph_name: graph_name)
          first_bnode = rest_bnode
        end
        if last
          object = item_to_rdf(last, graph_name: graph_name, &block)
          yield RDF::Statement(first_bnode, RDF.first, object, graph_name: graph_name)
          yield RDF::Statement(first_bnode, RDF.rest, RDF.nil, graph_name: graph_name)
        end
        result
      end

      ##
      # Create a new named node using the sequence
      def node
        RDF::Node.new(namer.get_sym)
      end
    end
  end
end


text:
# frozen_string_literal: true

require 'rdf/nquads'

module JSON
  module LD
    module FromRDF
      include Utils

      ##
      # Generate a JSON-LD array representation from an array of `RDF::Statement`.
      # Representation is in expanded form
      #
      # @param [Array<RDF::Statement>, RDF::Enumerable] dataset
      # @param [Boolean] useRdfType (false)
      #   If set to `true`, the JSON-LD processor will treat `rdf:type` like a normal property instead of using `@type`.
      # @param [Boolean] useNativeTypes (false) use native representations
      # @param extendedRepresentation (false)
      #   Use the extended internal representation for native types.
      #
      # @return [Array<Hash>] the JSON-LD document in normalized form
      def from_statements(dataset, useRdfType: false, useNativeTypes: false, extendedRepresentation: false)
        default_graph = {}
        graph_map = { '@default' => default_graph }
        referenced_once = {}

        value = nil

        # Create an entry for compound-literal node detection
        compound_literal_subjects = {}

        # Create a map for node to object representation

        # For each statement in dataset
        dataset.each do |statement|
          # log_debug("statement") { statement.to_nquads.chomp}

          name = if statement.graph_name
            @context.expand_iri(statement.graph_name,
              base: @options[:base]).to_s
          else
            '@default'
          end

          # Create a graph entry as needed
          node_map = graph_map[name] ||= {}
          compound_literal_subjects[name] ||= {}

          default_graph[name] ||= { '@id' => name } unless name == '@default'

          subject = if statement.subject.statement?
            resource_representation(statement.subject, useNativeTypes, extendedRepresentation)['@id'].to_json_c14n
          else
            statement.subject.to_s
          end
          node = node_map[subject] ||= resource_representation(statement.subject, useNativeTypes,
            extendedRepresentation)

          # If predicate is rdf:datatype, note subject in compound literal subjects map
          if @options[:rdfDirection] == 'compound-literal' && statement.predicate == RDF_DIRECTION
            compound_literal_subjects[name][subject] ||= true
          end

          # If object is an IRI, blank node identifier, or statement, 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.
          unless statement.object.literal?
            object = if statement.object.statement?
              resource_representation(statement.object, useNativeTypes, extendedRepresentation)['@id'].to_json_c14n
            else
              statement.object.to_s
            end
            node_map[object] ||=
              resource_representation(statement.object, useNativeTypes, extendedRepresentation)
          end

          # If predicate equals rdf:type, and object is an IRI or blank node identifier, append object to the value of the @type member of node. If no such member exists, create one and initialize it to an array whose only item is object. Finally, continue to the next RDF triple.
          if statement.predicate == RDF.type && statement.object.resource? && !useRdfType
            merge_value(node, '@type', statement.object.to_s)
            next
          end

          # Set value to the result of using the RDF to Object Conversion algorithm, passing object, rdfDirection, and use native types.
          value = resource_representation(statement.object, useNativeTypes, extendedRepresentation)

          merge_value(node, statement.predicate.to_s, value)

          # If object is a blank node identifier or rdf:nil, it might represent the a list node:
          if statement.object == RDF.nil
            # Append a new JSON object consisting of three members, node, property, and value to the usages array. The node member is set to a reference to node, property to predicate, and value to a reference to value.
            object = node_map[statement.object.to_s]
            merge_value(object, :usages, {
              node: node,
              property: statement.predicate.to_s,
              value: value
            })
          elsif referenced_once.key?(statement.object.to_s)
            referenced_once[statement.object.to_s] = false
          elsif statement.object.node?
            referenced_once[statement.object.to_s] = {
              node: node,
              property: statement.predicate.to_s,
              value: value
            }
          end
        end

        # For each name and graph object in graph map:
        graph_map.each do |name, graph_object|
          # If rdfDirection is compound-literal, check referenced_once for entries from compound_literal_subjects
          compound_literal_subjects.fetch(name, {}).each_key do |cl|
            node = referenced_once[cl][:node]
            next unless node.is_a?(Hash)

            property = referenced_once[cl][:property]
            value = referenced_once[cl][:value]
            cl_node = graph_map[name].delete(cl)
            next unless cl_node.is_a?(Hash)

            node[property].select do |v|
              next unless v['@id'] == cl

              v.delete('@id')
              v['@value'] = cl_node[RDF.value.to_s].first['@value']
              if (langs = cl_node[RDF_LANGUAGE.to_s])
                lang = langs.first['@value']
                unless /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(lang)
                  warn "i18n datatype language must be valid BCP47: #{lang.inspect}"
                end
                v['@language'] = lang
              end
              v['@direction'] = cl_node[RDF_DIRECTION.to_s].first['@value']
            end
          end

          nil_var = graph_object.fetch(RDF.nil.to_s, {})

          # For each item usage in the usages member of nil, perform the following steps:
          nil_var.fetch(:usages, []).each do |usage|
            node = usage[:node]
            property = usage[:property]
            head = usage[:value]
            list = []
            list_nodes = []

            # If property equals rdf:rest, the value associated to the usages member of node has exactly 1 entry, node has a rdf:first and rdf:rest property, both of which have as value an array consisting of a single element, and node has no other members apart from an optional @type member whose value is an array with a single item equal to rdf:List, node represents a well-formed list node. Continue with the following steps:
            # log_debug("list element?") {node.to_json(JSON_STATE) rescue 'malformed json'}
            while property == RDF.rest.to_s &&
                  blank_node?(node) &&
                  referenced_once[node['@id']] &&
                  node.keys.none? { |k| !["@id", '@type', :usages, RDF.first.to_s, RDF.rest.to_s].include?(k) } &&
                  (f = node[RDF.first.to_s]).is_a?(Array) && f.length == 1 &&
                  (r = node[RDF.rest.to_s]).is_a?(Array) && r.length == 1 &&
                  ((t = node['@type']).nil? || t == [RDF.List.to_s])
              list << Array(node[RDF.first.to_s]).first
              list_nodes << node['@id']

              # get next node, moving backwards through list
              node_usage = referenced_once[node['@id']]
              node = node_usage[:node]
              property = node_usage[:property]
              head = node_usage[:value]
            end

            head.delete('@id')
            head['@list'] = list.reverse
            list_nodes.each { |node_id| graph_object.delete(node_id) }
          end

          # Create annotations on graph object
          create_annotations(graph_object)
        end

        result = []
        default_graph.keys.opt_sort(ordered: @options[:ordered]).each do |subject|
          node = default_graph[subject]
          if graph_map.key?(subject)
            node['@graph'] = []
            graph_map[subject].keys.opt_sort(ordered: @options[:ordered]).each do |s|
              n = graph_map[subject][s]
              n.delete(:usages)
              node['@graph'] << n unless node_reference?(n)
            end
          end
          node.delete(:usages)
          result << node unless node_reference?(node)
        end
        # log_debug("fromRdf") {result.to_json(JSON_STATE) rescue 'malformed json'}
        result
      end

      private

      RDF_LITERAL_NATIVE_TYPES = Set.new([RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.double]).freeze

      def resource_representation(resource, useNativeTypes, extendedRepresentation)
        case resource
        when RDF::Statement
          # Note, if either subject or object are a BNode which is used elsewhere,
          # this might not work will with the BNode accounting from above.
          rep = { '@id' => resource_representation(resource.subject, false, extendedRepresentation) }
          if resource.predicate == RDF.type
            rep['@id']['@type'] = resource.object.to_s
          else
            rep['@id'][resource.predicate.to_s] =
              as_array(resource_representation(resource.object, useNativeTypes, extendedRepresentation))
          end
          rep
        when RDF::Literal
          base = @options[:base]
          rdfDirection = @options[:rdfDirection]
          res = {}

          if resource.datatype == RDF_JSON && @context.processingMode('json-ld-1.1')
            res['@type'] = '@json'
            res['@value'] = begin
              ::JSON.parse(resource.object)
            rescue ::JSON::ParserError => e
              raise JSON::LD::JsonLdError::InvalidJsonLiteral, e.message
            end
          elsif useNativeTypes && extendedRepresentation
            res['@value'] = resource  # Raw literal
          elsif resource.datatype.start_with?("https://www.w3.org/ns/i18n#") && rdfDirection == 'i18n-datatype' && @context.processingMode('json-ld-1.1')
            lang, dir = resource.datatype.fragment.split('_')
            res['@value'] = resource.to_s
            unless lang.empty?
              unless /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(lang)
                if options[:validate]
                  raise JsonLdError::InvalidLanguageMapping, "rdf:language must be valid BCP47: #{lang.inspect}"
                end

                warn "rdf:language must be valid BCP47: #{lang.inspect}"

              end
              res['@language'] = lang
            end
            res['@direction'] = dir
          elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(resource.datatype) && resource.valid?
            res['@value'] = resource.object
          else
            resource.canonicalize! if resource.valid? && resource.datatype == RDF::XSD.double
            if resource.datatype?
              res['@type'] = resource.datatype.to_s
            elsif resource.language?
              res['@language'] = resource.language.to_s
            end
            res['@value'] = resource.to_s
          end
          res
        else
          { '@id' => resource.to_s }
        end
      end
    end
  end
end


text:
# frozen_string_literal: true

require 'json/ld/streaming_writer'
require 'link_header'

module JSON
  module LD
    ##
    # A JSON-LD parser in Ruby.
    #
    # Note that the natural interface is to write a whole graph at a time.
    # Writing statements or Triples will create a graph to add them to
    # and then serialize the graph.
    #
    # @example Obtaining a JSON-LD writer class
    #   RDF::Writer.for(:jsonld)         #=> JSON::LD::Writer
    #   RDF::Writer.for("etc/test.json")
    #   RDF::Writer.for(:file_name      => "etc/test.json")
    #   RDF::Writer.for(file_extension: "json")
    #   RDF::Writer.for(:content_type   => "application/turtle")
    #
    # @example Serializing RDF graph into an JSON-LD file
    #   JSON::LD::Writer.open("etc/test.json") do |writer|
    #     writer << graph
    #   end
    #
    # @example Serializing RDF statements into an JSON-LD file
    #   JSON::LD::Writer.open("etc/test.json") do |writer|
    #     graph.each_statement do |statement|
    #       writer << statement
    #     end
    #   end
    #
    # @example Serializing RDF statements into an JSON-LD string
    #   JSON::LD::Writer.buffer do |writer|
    #     graph.each_statement do |statement|
    #       writer << statement
    #     end
    #   end
    #
    # The writer will add prefix definitions, and use them for creating @context definitions, and minting CURIEs
    #
    # @example Creating @@context prefix definitions in output
    #   JSON::LD::Writer.buffer(
    #     prefixes: {
    #       nil => "http://example.com/ns#",
    #       foaf: "http://xmlns.com/foaf/0.1/"}
    #   ) do |writer|
    #     graph.each_statement do |statement|
    #       writer << statement
    #     end
    #   end
    #
    # Select the :expand option to output JSON-LD in expanded form
    #
    # @see https://www.w3.org/TR/json-ld11-api/
    # @see https://www.w3.org/TR/json-ld11-api/#the-normalization-algorithm
    # @author [Gregg Kellogg](http://greggkellogg.net/)
    class Writer < RDF::Writer
      include StreamingWriter
      include Utils
      include RDF::Util::Logger
      format Format

      # @!attribute [r] graph
      # @return [RDF::Graph] Graph of statements serialized
      attr_reader :graph

      # @!attribute [r] context
      # @return [Context] context used to load and administer contexts
      attr_reader :context

      ##
      # JSON-LD Writer options
      # @see https://ruby-rdf.github.io/rdf/RDF/Writer#options-class_method
      def self.options
        super + [
          RDF::CLI::Option.new(
            symbol: :compactArrays,
            datatype: TrueClass,
            default: true,
            control: :checkbox,
            on: ["--[no-]compact-arrays"],
            description: "Replaces arrays with just one element with that element during compaction. Default is `true` use --no-compact-arrays to disable."
          ) { |arg| arg },
          RDF::CLI::Option.new(
            symbol: :compactToRelative,
            datatype: TrueClass,
            default: true,
            control: :checkbox,
            on: ["--[no-]compact-to-relative"],
            description: "Creates document relative IRIs when compacting, if `true`, otherwise leaves expanded. Default is `true` use --no-compact-to-relative to disable."
          ) { |arg| arg },
          RDF::CLI::Option.new(
            symbol: :context,
            datatype: RDF::URI,
            control: :url2,
            on: ["--context CONTEXT"],
            description: "Context to use when compacting."
          ) { |arg| RDF::URI(arg).absolute? ? RDF::URI(arg) : StringIO.new(File.read(arg)) },
          RDF::CLI::Option.new(
            symbol: :embed,
            datatype: %w[@always @once @never],
            default: '@once',
            control: :select,
            on: ["--embed EMBED"],
            description: "How to embed matched objects (@once)."
          ) { |arg| RDF::URI(arg) },
          RDF::CLI::Option.new(
            symbol: :explicit,
            datatype: TrueClass,
            control: :checkbox,
            on: ["--[no-]explicit"],
            description: "Only include explicitly declared properties in output (false)"
          ) { |arg| arg },
          RDF::CLI::Option.new(
            symbol: :frame,
            datatype: RDF::URI,
            control: :url2,
            use: :required,
            on: ["--frame FRAME"],
            description: "Frame to use when serializing."
          ) { |arg| RDF::URI(arg).absolute? ? RDF::URI(arg) : StringIO.new(File.read(arg)) },
          RDF::CLI::Option.new(
            symbol: :lowercaseLanguage,
            datatype: TrueClass,
            control: :checkbox,
            on: ["--[no-]lowercase-language"],
            description: "By default, language tags are left as is. To normalize to lowercase, set this option to `true`."
          ),
          RDF::CLI::Option.new(
            symbol: :omitDefault,
            datatype: TrueClass,
            control: :checkbox,
            on: ["--[no-]omitDefault"],
            description: "Omit missing properties from output (false)"
          ) { |arg| arg },
          RDF::CLI::Option.new(
            symbol: :ordered,
            datatype: TrueClass,
            control: :checkbox,
            on: ["--[no-]ordered"],
            description: "Order object member processing lexographically."
          ) { |arg| arg },
          RDF::CLI::Option.new(
            symbol: :processingMode,
            datatype: %w[json-ld-1.0 json-ld-1.1],
            control: :radio,
            on: ["--processingMode MODE", %w[json-ld-1.0 json-ld-1.1]],
            description: "Set Processing Mode (json-ld-1.0 or json-ld-1.1)"
          ),
          RDF::CLI::Option.new(
            symbol: :rdfDirection,
            datatype: %w[i18n-datatype compound-literal],
            default: 'null',
            control: :select,
            on: ["--rdf-direction DIR", %w[i18n-datatype compound-literal]],
            description: "How to serialize literal direction (i18n-datatype compound-literal)"
          ) { |arg| arg },
          RDF::CLI::Option.new(
            symbol: :requireAll,
            datatype: TrueClass,
            default: true,
            control: :checkbox,
            on: ["--[no-]require-all"],
            description: "Require all properties to match (true). Default is `true` use --no-require-all to disable."
          ) { |arg| arg },
          RDF::CLI::Option.new(
            symbol: :stream,
            datatype: TrueClass,
            control: :checkbox,
            on: ["--[no-]stream"],
            description: "Do not attempt to optimize graph presentation, suitable for streaming large graphs."
          ) { |arg| arg },
          RDF::CLI::Option.new(
            symbol: :useNativeTypes,
            datatype: TrueClass,
            control: :checkbox,
            on: ["--[no-]use-native-types"],
            description: "Use native JSON values in value objects."
          ) { |arg| arg },
          RDF::CLI::Option.new(
            symbol: :useRdfType,
            datatype: TrueClass,
            control: :checkbox,
            on: ["--[no-]use-rdf-type"],
            description: "Treat `rdf:type` like a normal property instead of using `@type`."
          ) { |arg| arg }
        ]
      end

      class << self
        attr_reader :white_list, :black_list

        ##
        # Use parameters from accept-params to determine if the parameters are acceptable to invoke this writer. The `accept_params` will subsequently be provided to the writer instance.
        #
        # @param [Hash{Symbol => String}] accept_params
        # @yield [accept_params] if a block is given, returns the result of evaluating that block
        # @yieldparam [Hash{Symbol => String}] accept_params
        # @return [Boolean]
        # @see    http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
        def accept?(accept_params)
          if block_given?
            yield(accept_params)
          else
            true
          end
        end

        ##
        # Returns default context used for compacted profile without an explicit context URL
        # @return [String]
        def default_context
          @default_context || JSON::LD::DEFAULT_CONTEXT
        end

        ##
        # Sets default context used for compacted profile without an explicit context URL
        # @param [String] url
        attr_writer :default_context
      end

      ##
      # Initializes the JSON-LD writer instance.
      #
      # @param  [IO, File] output
      #   the output stream
      # @param  [Hash{Symbol => Object}] options
      #   any additional options
      # @option options [Encoding] :encoding     (Encoding::UTF_8)
      #   the encoding to use on the output stream (Ruby 1.9+)
      # @option options [Boolean]  :canonicalize (false)
      #   whether to canonicalize literals when serializing
      # @option options [Hash]     :prefixes     ({})
      #   the prefix mappings to use (not supported by all writers)
      # @option options [Boolean]  :standard_prefixes   (false)
      #   Add standard prefixes to @prefixes, if necessary.
      # @option options [IO, Array, Hash, String, Context]     :context     ({})
      #   context to use when serializing. Constructed context for native serialization.
      # @option options [IO, Array, Hash, String, Context]     :frame     ({})
      #   frame to use when serializing.
      # @option options [Boolean]  :unique_bnodes   (false)
      #   Use unique bnode identifiers, defaults to using the identifier which the node was originall initialized with (if any).
      # @option options [Proc] serializer (JSON::LD::API.serializer)
      #   A Serializer method used for generating the JSON serialization of the result.
      # @option options [Boolean] :stream (false)
      #   Do not attempt to optimize graph presentation, suitable for streaming large graphs.
      # @yield  [writer] `self`
      # @yieldparam  [RDF::Writer] writer
      # @yieldreturn [void]
      # @yield  [writer]
      # @yieldparam [RDF::Writer] writer
      def initialize(output = $stdout, **options, &block)
        options[:base_uri] ||= options[:base] if options.key?(:base)
        options[:base] ||= options[:base_uri] if options.key?(:base_uri)
        @serializer = options.fetch(:serializer, JSON::LD::API.method(:serializer))
        super do
          @repo = RDF::Repository.new

          if block
            case block.arity
            when 0 then instance_eval(&block)
            else yield(self)
            end
          end
        end
      end

      ##
      # Addes a triple to be serialized
      # @param  [RDF::Resource] subject
      # @param  [RDF::URI]      predicate
      # @param  [RDF::Value]    object
      # @return [void]
      # @abstract
      def write_triple(subject, predicate, object)
        write_quad(subject, predicate, object, nil)
      end

      ##
      # Outputs the N-Quads representation of a statement.
      #
      # @param  [RDF::Resource] subject
      # @param  [RDF::URI]      predicate
      # @param  [RDF::Term]     object
      # @return [void]
      def write_quad(subject, predicate, object, graph_name)
        statement = RDF::Statement.new(subject, predicate, object, graph_name: graph_name)
        if @options[:stream]
          stream_statement(statement)
        else
          @repo.insert(statement)
        end
      end

      ##
      # Necessary for streaming
      # @return [void] `self`
      def write_prologue
        stream_prologue if @options[:stream]
        super
      end

      ##
      # Outputs the Serialized JSON-LD representation of all stored statements.
      #
      # If provided a context or prefixes, we'll create a context
      # and use it to compact the output. Otherwise, we return un-compacted JSON-LD
      #
      # @return [void]
      # @see    #write_triple
      def write_epilogue
        if @options[:stream]
          stream_epilogue
        else

          # log_debug("writer") { "serialize #{@repo.count} statements, #{@options.inspect}"}
          result = API.fromRdf(@repo, **@options.merge(serializer: nil))

          # Some options may be indicated from accept parameters
          profile = @options.fetch(:accept_params, {}).fetch(:profile, "").split
          links = LinkHeader.parse(@options[:link])
          @options[:context] ||= begin
            links.find_link(['rel', JSON_LD_NS + "context"]).href
          rescue StandardError
            nil
          end
          @options[:context] ||= Writer.default_context if profile.include?(JSON_LD_NS + "compacted")
          @options[:frame] ||= begin
            links.find_link(['rel', JSON_LD_NS + "frame"]).href
          rescue StandardError
            nil
          end

          # If we were provided a context, or prefixes, use them to compact the output
          context = @options[:context]
          context ||= if @options[:prefixes] || @options[:language] || @options[:standard_prefixes]
            ctx = Context.new(**@options)
            ctx.language = @options[:language] if @options[:language]
            @options[:prefixes]&.each do |prefix, iri|
              ctx.set_mapping(prefix, iri) if prefix && iri
            end
            ctx
          end

          # Rename BNodes to uniquify them, if necessary
          result = API.flatten(result, context, **@options.merge(serializer: nil)) if options[:unique_bnodes]

          if (frame = @options[:frame])
            # Perform framing, if given a frame
            # log_debug("writer") { "frame result"}
            result = API.frame(result, frame, **@options.merge(serializer: nil))
          elsif context
            # Perform compaction, if we have a context
            # log_debug("writer") { "compact result"}
            result = API.compact(result, context, **@options.merge(serializer: nil))
          end

          @output.write(@serializer.call(result, **@options))
        end

        super
      end
    end
  end
end


text:
# frozen_string_literal: true

require 'htmlentities'

module JSON
  module LD
    class API
      ##
      # REXML implementation of an XML parser.
      #
      # @see http://www.germane-software.com/software/rexml/
      module REXML
        ##
        # Returns the name of the underlying XML library.
        #
        # @return [Symbol]
        def self.library
          :rexml
        end

        # Proxy class to implement uniform element accessors
        class NodeProxy
          attr_reader :node, :parent

          def initialize(node, parent = nil)
            @node = node
            @parent = parent
          end

          ##
          # Return xml:base on element, if defined
          #
          # @return [String]
          def base
            @node.attribute("base", RDF::XML.to_s) || @node.attribute('xml:base')
          end

          def display_path
            @display_path ||= begin
              path = []
              path << parent.display_path if parent
              path << @node.name
              case @node
              when ::REXML::Element   then path.join("/")
              when ::REXML::Attribute then path.join("@")
              else path.join("?")
              end
            end
          end

          ##
          # Return true of all child elements are text
          #
          # @return [Array<:text, :element, :attribute>]
          def text_content?
            @node.children.all?(::REXML::Text)
          end

          ##
          # Children of this node
          #
          # @return [NodeSetProxy]
          def children
            NodeSetProxy.new(@node.children, self)
          end

          # Ancestors of this element, in order
          def ancestors
            @ancestors ||= parent ? parent.ancestors + [parent] : []
          end

          ##
          # Inner text of an element
          #
          # @see http://apidock.com/ruby/REXML/Element/get_text#743-Get-all-inner-texts
          # @return [String]
          def inner_text
            coder = HTMLEntities.new
            ::REXML::XPath.match(@node, './/text()').map do |e|
              coder.decode(e)
            end.join
          end

          ##
          # Inner text of an element
          #
          # @see http://apidock.com/ruby/REXML/Element/get_text#743-Get-all-inner-texts
          # @return [String]
          def inner_html
            @node.children.map(&:to_s).join
          end

          def attribute_nodes
            attrs = @node.attributes.dup.keep_if do |name, _attr|
              !name.start_with?('xmlns')
            end
            @attribute_nodes ||= (attrs.empty? ? attrs : NodeSetProxy.new(attrs, self))
          end

          ##
          # Node type accessors
          #
          # @return [Boolean]
          def text?
            @node.is_a?(::REXML::Text)
          end

          def element?
            @node.is_a?(::REXML::Element)
          end

          def blank?
            @node.is_a?(::REXML::Text) && @node.empty?
          end

          def to_s
            @node.to_s
          end

          def xpath(*args)
            ::REXML::XPath.match(@node, *args).map do |n|
              NodeProxy.new(n, parent)
            end
          end

          def at_xpath(*args)
            xpath(*args).first
          end

          ##
          # Proxy for everything else to @node
          def method_missing(method, *args)
            @node.send(method, *args)
          end
        end

        ##
        # NodeSet proxy
        class NodeSetProxy
          attr_reader :node_set, :parent

          def initialize(node_set, parent)
            @node_set = node_set
            @parent = parent
          end

          ##
          # Return a proxy for each child
          #
          # @yield child
          # @yieldparam [NodeProxy]
          def each
            @node_set.each do |c|
              yield NodeProxy.new(c, parent)
            end
          end

          ##
          def to_html
            node_set.map(&:to_s).join
          end

          ##
          # Proxy for everything else to @node_set
          def method_missing(method, *args)
            @node_set.send(method, *args)
          end
        end

        ##
        # Initializes the underlying XML library.
        #
        # @param  [Hash{Symbol => Object}] options
        # @return [NodeProxy] of document root
        def initialize_html_rexml(input, _options = {})
          require 'rexml/document' unless defined?(::REXML)
          doc = case input
          when ::REXML::Document
            input
          else
            # Only parse as XML, no HTML mode
            ::REXML::Document.new(input.respond_to?(:read) ? input.read : input.to_s)
          end

          NodeProxy.new(doc.root) if doc&.root
        end
        alias initialize_html initialize_html_rexml
      end
    end
  end
end


text:
# frozen_string_literal: true

module JSON
  module LD
    class API
      ##
      # Nokogiri implementation of an HTML parser.
      #
      # @see http://nokogiri.org/
      module Nokogiri
        ##
        # Returns the name of the underlying XML library.
        #
        # @return [Symbol]
        def self.library
          :nokogiri
        end

        # Proxy class to implement uniform element accessors
        class NodeProxy
          attr_reader :node, :parent

          def initialize(node, parent = nil)
            @node = node
            @parent = parent
          end

          ##
          # Return xml:base on element, if defined
          #
          # @return [String]
          def base
            @node.attribute_with_ns("base", RDF::XML.to_s) || @node.attribute('xml:base')
          end

          def display_path
            @display_path ||= begin
              path = []
              path << parent.display_path if parent
              path << @node.name
              case @node
              when ::Nokogiri::XML::Element then path.join("/")
              when ::Nokogiri::XML::Attr    then path.join("@")
              else path.join("?")
              end
            end
          end

          ##
          # Return true of all child elements are text
          #
          # @return [Array<:text, :element, :attribute>]
          def text_content?
            @node.children.all?(&:text?)
          end

          ##
          # Children of this node
          #
          # @return [NodeSetProxy]
          def children
            NodeSetProxy.new(@node.children, self)
          end

          # Ancestors of this element, in order
          def ancestors
            @ancestors ||= parent ? parent.ancestors + [parent] : []
          end

          ##
          # Inner text of an element. Decode Entities
          #
          # @return [String]
          # def inner_text
          #  coder = HTMLEntities.new
          #  coder.decode(@node.inner_text)
          # end

          def attribute_nodes
            @attribute_nodes ||= NodeSetProxy.new(@node.attribute_nodes, self)
          end

          def xpath(*args)
            @node.xpath(*args).map do |n|
              # Get node ancestors
              parent = n.ancestors.reverse.inject(nil) do |p, node|
                NodeProxy.new(node, p)
              end
              NodeProxy.new(n, parent)
            end
          end

          ##
          # Proxy for everything else to @node
          def method_missing(method, *args)
            @node.send(method, *args)
          end
        end

        ##
        # NodeSet proxy
        class NodeSetProxy
          attr_reader :node_set, :parent

          def initialize(node_set, parent)
            @node_set = node_set
            @parent = parent
          end

          ##
          # Return a proxy for each child
          #
          # @yield child
          # @yieldparam [NodeProxy]
          def each
            @node_set.each do |c|
              yield NodeProxy.new(c, parent)
            end
          end

          ##
          # Proxy for everything else to @node_set
          def method_missing(method, *args)
            @node_set.send(method, *args)
          end
        end

        ##
        # Initializes the underlying XML library.
        #
        # @param  [Hash{Symbol => Object}] options
        # @return [NodeProxy] of root element
        def initialize_html_nokogiri(input, _options = {})
          require 'nokogiri' unless defined?(::Nokogiri)
          doc = case input
          when ::Nokogiri::HTML::Document, ::Nokogiri::XML::Document
            input
          else
            begin
              input = input.read if input.respond_to?(:read)
              ::Nokogiri::HTML5(input.force_encoding('utf-8'), max_parse_errors: 1000)
            rescue LoadError, NoMethodError
              ::Nokogiri::HTML.parse(input, base_uri.to_s, 'utf-8')
            end
          end

          NodeProxy.new(doc.root) if doc&.root
        end
        alias initialize_html initialize_html_nokogiri
      end
    end
  end
end


text:
#!/usr/bin/env ruby
require 'rubygems'
$:.unshift(File.expand_path("../../lib", __FILE__))
require 'json/ld'
require 'getoptlong'

def run(options)
  parser_options = options[:parser_options].merge(standard_prefixes: true)
  statement = RDF::Statement(RDF::URI("http://example/a"), RDF::URI("http://example/b"), RDF::Literal("c"))
  start = Time.new
  num = 100_000
  RDF::Writer.for(options[:output_format]).new(options[:output], parser_options) do |w|
    (1..num).each do
      w << statement
    end
  end
  secs = Time.new - start
  STDERR.puts "\nProcessed #{num} statements in #{secs} seconds @ #{num/secs} statements/second." unless options[:quiet]
end

parser_options = {
  base:     nil,
  progress: false,
  validate: false,
  stream:   false,
  strict:   false,
}

options = {
  parser_options: parser_options,
  output:         STDOUT,
  output_format:  :jsonld,
  input_format:   :jsonld,
}
input = nil

OPT_ARGS = [
  ["--output", "-o",  GetoptLong::REQUIRED_ARGUMENT,"Output to the specified file path"],
  ["--stream",        GetoptLong::NO_ARGUMENT,      "Use Streaming reader/writer"],
  ["--help", "-?",    GetoptLong::NO_ARGUMENT,      "This message"]
]
def usage
  STDERR.puts %{Usage: #{$0} [options] file ...}
  width = OPT_ARGS.map do |o|
    l = o.first.length
    l += o[1].length + 2 if o[1].is_a?(String)
    l
  end.max
  OPT_ARGS.each do |o|
    s = "  %-*s  " % [width, (o[1].is_a?(String) ? "#{o[0,2].join(', ')}" : o[0])]
    s += o.last
    STDERR.puts s
  end
  exit(1)
end

opts = GetoptLong.new(*OPT_ARGS.map {|o| o[0..-2]})

opts.each do |opt, arg|
  case opt
  when '--output'       then options[:output] = File.open(arg, "w")
  when '--quiet'        then options[:quiet] = true
  when '--stream'       then parser_options[:stream] = true
  when '--help'         then usage
  end
end

run(options)


text:
<?xml version="1.0"?>

<!DOCTYPE rdf:RDF [
    <!ENTITY schema "http://schema.org/" >
    <!ENTITY owl "http://www.w3.org/2002/07/owl#" >
    <!ENTITY terms "http://data.wikia.com/terms.owl#" >
    <!ENTITY xsd "http://www.w3.org/2001/XMLSchema#" >
    <!ENTITY rdfs "http://www.w3.org/2000/01/rdf-schema#" >
    <!ENTITY rdf "http://www.w3.org/1999/02/22-rdf-syntax-ns#" >
]>

<rdf:RDF xmlns="http://data.wikia.com/terms#"
     xml:base="http://data.wikia.com/terms"
     xmlns:schema="http://schema.org/"
     xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
     xmlns:terms="http://data.wikia.com/terms.owl#"
     xmlns:owl="http://www.w3.org/2002/07/owl#"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

    <owl:Class rdf:about="http://data.wikia.com/terms#Character">
        <rdfs:subClassOf>
            <owl:Restriction>
                <owl:onProperty rdf:resource="http://data.wikia.com/terms#characterIn"/>
                <owl:onClass rdf:resource="http://data.wikia.com/terms#Element"/>
                <owl:minQualifiedCardinality rdf:datatype="&xsd;nonNegativeInteger">1</owl:minQualifiedCardinality>
            </owl:Restriction>
        </rdfs:subClassOf>
    </owl:Class>
</rdf:RDF>

<!-- Generated by the OWL API (version 3.2.3.1824) http://owlapi.sourceforge.net -->