text:
# JSON-LD reader/writer

[JSON-LD][] reader/writer for [RDF.rb][RDF.rb] and fully conforming [JSON-LD API][] processor. Additionally this gem implements [JSON-LD Framing][].

[![Gem Version](https://badge.fury.io/rb/json-ld.svg)](https://rubygems.org/gems/json-ld)
[![Build Status](https://github.com/ruby-rdf/json-ld/actions/workflows/ci.yml/badge.svg)](https://github.com/ruby-rdf/json-ld/actions?query=workflow%3ACI)
[![Coverage Status](https://coveralls.io/repos/ruby-rdf/json-ld/badge.svg?branch=develop)](https://coveralls.io/github/ruby-rdf/json-ld?branch=develop)
[![Gitter chat](https://badges.gitter.im/ruby-rdf.png)](https://gitter.im/gitterHQ/gitter)

## Features

JSON::LD parses and serializes [JSON-LD][] into [RDF][] and implements expansion, compaction and framing API interfaces. It also extracts JSON-LD from HTML.

JSON::LD can now be used to create a _context_ from an RDFS/OWL definition, and optionally include a JSON-LD representation of the ontology itself. This is currently accessed through the `script/gen_context` script.

* If the [jsonlint][] gem is installed, it will be used when validating an input document.
* If available, uses [Nokogiri][] for parsing HTML, falls back to REXML otherwise.
* Provisional support for [JSON-LD-star][JSON-LD-star].

[Implementation Report](https://ruby-rdf.github.io/json-ld/etc/earl.html)

Install with `gem install json-ld`

### JSON-LD Streaming Profile
This gem implements an optimized streaming reader used for generating RDF from large dataset dumps formatted as JSON-LD. Such documents must correspond to the [JSON-LD Streaming Profile](https://w3c.github.io/json-ld-streaming/):

* Keys in JSON objects must be ordered with any of `@context`, and/or `@type` coming before any other keys, in that order. This includes aliases of those keys. It is strongly encouraged that `@id` be present, and come immediately after.
* JSON-LD documents can be signaled or requested in [streaming document form](https://w3c.github.io/json-ld-streaming/#dfn-streaming-document-form). The profile URI identifying the [streaming document form](https://w3c.github.io/json-ld-streaming/#dfn-streaming-document-form) is `http://www.w3.org/ns/json-ld#streaming`.

This gem also implements an optimized streaming writer used for generating JSON-LD from large repositories. Such documents result in the JSON-LD Streaming Profile:

* Each statement written as a separate node in expanded/flattened form.
* `RDF List`s are written as separate nodes using `rdf:first` and `rdf:rest` properties.

The order of triples retrieved from the `RDF::Enumerable` dataset determines the way that JSON-LD node objects are written; for best results, statements should be ordered by _graph name_, _subject_, _predicate_ and _object_.

### MultiJson parser
The [MultiJson](https://rubygems.org/gems/multi_json) gem is used for parsing and serializing JSON; this defaults to the native JSON parser/serializer, but will use a more performant parser if one is available. A specific parser can be specified by adding the `:adapter` option to any API call. Additionally, a custom serialilzer may be specified by passing the `:serializer` option to {JSON::LD::Writer} or methods of {JSON::LD::API}. See [MultiJson](https://rubygems.org/gems/multi_json) for more information.

### JSON-LD-star (RDFStar)

The {JSON::LD::API.expand}, {JSON::LD::API.compact}, {JSON::LD::API.toRdf}, and {JSON::LD::API.fromRdf} API methods, along with the {JSON::LD::Reader} and {JSON::LD::Writer}, include provisional support for [JSON-LD-star][JSON-LD-star].

Internally, an `RDF::Statement` is treated as another resource, along with `RDF::URI` and `RDF::Node`, which allows an `RDF::Statement` to have a `#subject` or `#object` which is also an `RDF::Statement`.

In JSON-LD, with the `rdfstar` option set, the value of `@id`, in addition to an IRI or Blank Node Identifier, can be a JSON-LD node object having exactly one property with an optional `@id`, which may also be an embedded object. (It may also have `@context` and `@index` values).

    {
      "@id": {
        "@context": {"foaf": "http://xmlns.com/foaf/0.1/"},
        "@index": "ignored",
        "@id": "bob",
        "foaf:age" 23
      },
      "ex:certainty": 0.9
    }

Additionally, the `@annotation` property (or alias) may be used on a node object or value object to annotate the statement for which the associated node is the object of a triple.

    {
      "@context": {"foaf": "http://xmlns.com/foaf/0.1/"},
      "@id": "bob",
      "foaf:age" 23,
      "@annotation": {
        "ex:certainty": 0.9
      }
    }

In the first case, the embedded node is not asserted, and only appears as the subject of a triple. In the second case, the triple is asserted and used as the subject in another statement which annotates it.

**Note: This feature is subject to change or elimination as the standards process progresses.**

#### Serializing a Graph containing embedded statements

    require 'json/ld'
    statement = RDF::Statement(RDF::URI('bob'), RDF::Vocab::FOAF.age, RDF::Literal(23))
    graph = RDF::Graph.new << [statement, RDF::URI("ex:certainty"), RDF::Literal(0.9)]
    graph.dump(:jsonld, validate: false, standard_prefixes: true)
    # => {"@id": {"@id": "bob", "foaf:age" 23}, "ex:certainty": 0.9}

Alternatively, using the {JSON::LD::API.fromRdf} method:

    JSON::LD::API::fromRdf(graph)
    # => {"@id": {"@id": "bob", "foaf:age" 23}, "ex:certainty": 0.9}

#### Reading a Graph containing embedded statements

By default, {JSON::LD::API.toRdf} (and {JSON::LD::Reader}) will reject a document containing a subject resource.

    jsonld = %({
      "@id": {
        "@id": "bob", "foaf:age" 23
      },
      "ex:certainty": 0.9
    })
    graph = RDF::Graph.new << JSON::LD::API.toRdf(input)
    # => JSON::LD::JsonLdError::InvalidIdValue

{JSON::LD::API.toRdf} (and {JSON::LD::Reader}) support a boolean valued `rdfstar` option; only one statement is asserted, although the reified statement is contained within the graph.

    graph = RDF::Graph.new do |graph|
      JSON::LD::Reader.new(jsonld, rdfstar: true) {|reader| graph << reader}
    end
    graph.count #=> 1

## Examples

```ruby
require 'rubygems'
require 'json/ld'
 ```

### Expand a Document

```ruby
   input = JSON.parse %({
      "@context": {
        "name": "http://xmlns.com/foaf/0.1/name",
        "homepage": "http://xmlns.com/foaf/0.1/homepage",
        "avatar": "http://xmlns.com/foaf/0.1/avatar"
      },
      "name": "Manu Sporny",
      "homepage": "http://manu.sporny.org/",
      "avatar": "http://twitter.com/account/profile_image/manusporny"
    })
    JSON::LD::API.expand(input) =>

    [{
        "http://xmlns.com/foaf/0.1/name": [{"@value"=>"Manu Sporny"}],
        "http://xmlns.com/foaf/0.1/homepage": [{"@value"=>"https://manu.sporny.org/"}],
        "http://xmlns.com/foaf/0.1/avatar": [{"@value": "https://twitter.com/account/profile_image/manusporny"}]
    }]
```

### Compact a Document

    input = JSON.parse %([{
        "http://xmlns.com/foaf/0.1/name": ["Manu Sporny"],
        "http://xmlns.com/foaf/0.1/homepage": [{"@id": "https://manu.sporny.org/"}],
        "http://xmlns.com/foaf/0.1/avatar": [{"@id": "https://twitter.com/account/profile_image/manusporny"}]
    }])

    context = JSON.parse(%({
      "@context": {
        "name": "http://xmlns.com/foaf/0.1/name",
        "homepage": {"@id": "http://xmlns.com/foaf/0.1/homepage", "@type": "@id"},
        "avatar": {"@id": "http://xmlns.com/foaf/0.1/avatar", "@type": "@id"}
      }
    }))['@context']

    JSON::LD::API.compact(input, context) =>
    {
        "@context": {
          "name": "http://xmlns.com/foaf/0.1/name",
          "homepage": {"@id": "http://xmlns.com/foaf/0.1/homepage", "@type": "@id"},
          "avatar": {"@id": "http://xmlns.com/foaf/0.1/avatar", "@type": "@id"}
        },
        "avatar": "https://twitter.com/account/profile_image/manusporny",
        "homepage": "https://manu.sporny.org/",
        "name": "Manu Sporny"
    }

### Frame a Document

    input = JSON.parse %({
      "@context": {
        "Book":         "http://example.org/vocab#Book",
        "Chapter":      "http://example.org/vocab#Chapter",
        "contains":     {"@id": "http://example.org/vocab#contains", "@type": "@id"},
        "creator":      "http://purl.org/dc/terms/creator",
        "description":  "http://purl.org/dc/terms/description",
        "Library":      "http://example.org/vocab#Library",
        "title":        "http://purl.org/dc/terms/title"
      },
      "@graph":
      [{
        "@id": "http://example.com/library",
        "@type": "Library",
        "contains": "http://example.org/library/the-republic"
      },
      {
        "@id": "http://example.org/library/the-republic",
        "@type": "Book",
        "creator": "Plato",
        "title": "The Republic",
        "contains": "http://example.org/library/the-republic#introduction"
      },
      {
        "@id": "http://example.org/library/the-republic#introduction",
        "@type": "Chapter",
        "description": "An introductory chapter on The Republic.",
        "title": "The Introduction"
      }]
    })

    frame = JSON.parse %({
      "@context": {
        "Book":         "http://example.org/vocab#Book",
        "Chapter":      "http://example.org/vocab#Chapter",
        "contains":     "http://example.org/vocab#contains",
        "creator":      "http://purl.org/dc/terms/creator",
        "description":  "http://purl.org/dc/terms/description",
        "Library":      "http://example.org/vocab#Library",
        "title":        "http://purl.org/dc/terms/title"
      },
      "@type": "Library",
      "contains": {
        "@type": "Book",
        "contains": {
          "@type": "Chapter"
        }
      }
    })

    JSON::LD::API.frame(input, frame) =>
    {
      "@context": {
        "Book": "http://example.org/vocab#Book",
        "Chapter": "http://example.org/vocab#Chapter",
        "contains": "http://example.org/vocab#contains",
        "creator": "http://purl.org/dc/terms/creator",
        "description": "http://purl.org/dc/terms/description",
        "Library": "http://example.org/vocab#Library",
        "title": "http://purl.org/dc/terms/title"
      },
      "@graph": [
        {
          "@id": "http://example.com/library",
          "@type": "Library",
          "contains": {
            "@id": "http://example.org/library/the-republic",
            "@type": "Book",
            "contains": {
              "@id": "http://example.org/library/the-republic#introduction",
              "@type": "Chapter",
              "description": "An introductory chapter on The Republic.",
              "title": "The Introduction"
            },
            "creator": "Plato",
            "title": "The Republic"
          }
        }
      ]
    }

### Turn JSON-LD into RDF (Turtle)

    input = JSON.parse %({
      "@context": {
        "":       "https://manu.sporny.org/",
        "foaf":   "http://xmlns.com/foaf/0.1/"
      },
      "@id":       "http://example.org/people#joebob",
      "@type":          "foaf:Person",
      "foaf:name":      "Joe Bob",
      "foaf:nick":      { "@list": [ "joe", "bob", "jaybe" ] }
    })

    graph = RDF::Graph.new << JSON::LD::API.toRdf(input)

    require 'rdf/turtle'
    graph.dump(:ttl, prefixes: {foaf: "http://xmlns.com/foaf/0.1/"})
    @prefix foaf: <http://xmlns.com/foaf/0.1/> .

    <http://example.org/people#joebob> a foaf:Person;
       foaf:name "Joe Bob";
       foaf:nick ("joe" "bob" "jaybe") .

### Turn RDF into JSON-LD

    require 'rdf/turtle'
    input = RDF::Graph.new << RDF::Turtle::Reader.new(%(
      @prefix foaf: <http://xmlns.com/foaf/0.1/> .

      <https://manu.sporny.org/#me> a foaf:Person;
         foaf:knows [ a foaf:Person;
           foaf:name "Gregg Kellogg"];
         foaf:name "Manu Sporny" .
    ))

    context = JSON.parse %({
      "@context": {
        "":       "https://manu.sporny.org/",
        "foaf":   "http://xmlns.com/foaf/0.1/"
      }
    })

    compacted = nil
    JSON::LD::API::fromRdf(input) do |expanded|
      compacted = JSON::LD::API.compact(expanded, context['@context'])
    end
    compacted =>
      [
        {
          "@id": "_:g70265766605380",
          "@type": ["http://xmlns.com/foaf/0.1/Person"],
          "http://xmlns.com/foaf/0.1/name": [{"@value": "Gregg Kellogg"}]
        },
        {
          "@id": "https://manu.sporny.org/#me",
          "@type": ["http://xmlns.com/foaf/0.1/Person"],
          "http://xmlns.com/foaf/0.1/knows": [{"@id": "_:g70265766605380"}],
          "http://xmlns.com/foaf/0.1/name": [{"@value": "Manu Sporny"}]
        }
      ]

## Use a custom Document Loader
In some cases, the built-in document loader {JSON::LD::API.documentLoader} is inadequate; for example, when using `http://schema.org` as a remote context, it will be re-loaded every time (however, see [json-ld-preloaded](https://rubygems.org/gems/json-ld-preloaded)).

All entries into the {JSON::LD::API} accept a `:documentLoader` option, which can be used to provide an alternative method to use when loading remote documents. For example:

```ruby
load_document_local = Proc.new do |url, **options, &block|
  if RDF::URI(url, canonicalize: true) == RDF::URI('http://schema.org/')
    remote_document = JSON::LD::API::RemoteDocument.new(url, File.read("etc/schema.org.jsonld"))
    return block_given? ? yield(remote_document) : remote_document
  else
    JSON::LD::API.documentLoader(url, options, &block)
  end
end

```
Then, when performing something like expansion:

```ruby
JSON::LD::API.expand(input, documentLoader: load_document_local)
```

## Preloading contexts
In many cases, for small documents, processing time can be dominated by loading and parsing remote contexts. In particular, a small schema.org example may need to download a large context and turn it into an internal representation, before the actual document can be expanded for processing. Using {JSON::LD::Context.add_preloaded}, an implementation can perform this loading up-front, and make it available to the processor.

```ruby
    ctx = JSON::LD::Context.new().parse('http://schema.org/')
    JSON::LD::Context.add_preloaded('http://schema.org/', ctx)
 ```

On lookup, URIs with an `https` prefix are normalized to `http`.

A context may be serialized to Ruby to speed this process using `Context#to_rb`. When loaded, this generated file will add entries to the {JSON::LD::Context::PRELOADED}.

## RDF Reader and Writer
{JSON::LD} also acts as a normal RDF reader and writer, using the standard RDF.rb reader/writer interfaces:

```ruby
    graph = RDF::Graph.load("etc/doap.jsonld", format: :jsonld)
    graph.dump(:jsonld, standard_prefixes: true)
```

`RDF::GRAPH#dump` can also take a `:context` option to use a separately defined context

As JSON-LD may come from many different sources, included as an embedded script tag within an HTML document, the RDF Reader will strip input before the leading `{` or `[` and after the trailing `}` or `]`.

## Extensions from JSON-LD 1.0
This implementation is being used as a test-bed for features planned for an upcoming JSON-LD 1.1 Community release.

### Scoped Contexts
A term definition can include `@context`, which is applied to values of that object. This is also used when compacting. Taken together, this allows framing to effectively include context definitions more deeply within the framed structure.

    {
      "@context": {
        "ex": "http://example.com/",
        "foo": {
          "@id": "ex:foo",
          "@type": "@vocab"
          "@context": {
            "Bar": "ex:Bar",
            "Baz": "ex:Baz"
          }
        }
      },
      "foo": "Bar"
    }

### @id and @type maps
The value of `@container` in a term definition can include `@id` or `@type`, in addition to `@set`, `@list`, `@language`, and `@index`. This allows value indexing based on either the `@id` or `@type` of associated objects.

    {
      "@context": {
        "@vocab": "http://example/",
        "idmap": {"@container": "@id"}
      },
      "idmap": {
        "http://example.org/foo": {"label": "Object with @id <foo>"},
        "_:bar": {"label": "Object with @id _:bar"}
      }
    }

### @graph containers and maps
A term can have `@container` set to include `@graph` optionally including `@id` or `@index` and `@set`. In the first form, with `@container` set to `@graph`, the value of a property is treated as a _simple graph object_, meaning that values treated as if they were contained in an object with `@graph`, creating _named graph_ with an anonymous name.

    {
      "@context": {
        "@vocab": "http://example.org/",
        "input": {"@container": "@graph"}
      },
      "input": {
        "value": "x"
      }
    }

which expands to the following:

    [{
      "http://example.org/input": [{
        "@graph": [{
          "http://example.org/value": [{"@value": "x"}]
        }]
      }]
    }]

Compaction reverses this process, optionally ensuring that a single value is contained within an array of `@container` also includes `@set`:

    {
      "@context": {
        "@vocab": "http://example.org/",
        "input": {"@container": ["@graph", "@set"]}
      }
    }

A graph map uses the map form already existing for `@index`, `@language`, `@type`, and `@id` where the index is either an index value or an id.

    {
      "@context": {
        "@vocab": "http://example.org/",
        "input": {"@container": ["@graph", "@index"]}
      },
      "input": {
        "g1": {"value": "x"}
      }
    }

treats "g1" as an index, and expands to the following:

    [{
      "http://example.org/input": [{
        "@index": "g1",
        "@graph": [{
          "http://example.org/value": [{"@value": "x"}]
        }]
      }]
    }])

This can also include `@set` to ensure that, when compacting, a single value of an index will be in array form.

The _id_ version is similar:

    {
      "@context": {
        "@vocab": "http://example.org/",
        "input": {"@container": ["@graph", "@id"]}
      },
      "input": {
        "http://example.com/g1": {"value": "x"}
      }
    }

which expands to:

    [{
      "http://example.org/input": [{
        "@id": "http://example.com/g1",
        "@graph": [{
          "http://example.org/value": [{"@value": "x"}]
        }]
      }]
    }])

### Transparent Nesting
Many JSON APIs separate properties from their entities using an intermediate object. For example, a set of possible labels may be grouped under a common property:

    {
      "@context": {
        "skos": "http://www.w3.org/2004/02/skos/core#",
        "labels": "@nest",
        "main_label": {"@id": "skos:prefLabel"},
        "other_label": {"@id": "skos:altLabel"},
        "homepage": {"@id":"http://schema.org/description", "@type":"@id"}
      },
      "@id":"http://example.org/myresource",
      "homepage": "http://example.org",
      "labels": {
         "main_label": "This is the main label for my resource",
         "other_label": "This is the other label"
      }
    }

 In this case, the `labels` property is semantically meaningless. Defining it as equivalent to `@nest` causes it to be ignored when expanding, making it equivalent to the following:

    {
      "@context": {
        "skos": "http://www.w3.org/2004/02/skos/core#",
        "labels": "@nest",
        "main_label": {"@id": "skos:prefLabel"},
        "other_label": {"@id": "skos:altLabel"},
        "homepage": {"@id":"http://schema.org/description", "@type":"@id"}
      },
      "@id":"http://example.org/myresource",
      "homepage": "http://example.org",
      "main_label": "This is the main label for my resource",
      "other_label": "This is the other label"
    }

 Similarly, properties may be marked with "@nest": "nest-term", to cause them to be nested. Note that the `@nest` keyword can also be aliased in the context.

     {
       "@context": {
         "skos": "http://www.w3.org/2004/02/skos/core#",
         "labels": "@nest",
         "main_label": {"@id": "skos:prefLabel", "@nest": "labels"},
         "other_label": {"@id": "skos:altLabel", "@nest": "labels"},
         "homepage": {"@id":"http://schema.org/description", "@type":"@id"}
       },
       "@id":"http://example.org/myresource",
       "homepage": "http://example.org",
       "labels": {
          "main_label": "This is the main label for my resource",
          "other_label": "This is the other label"
       }
     }

In this way, nesting survives round-tripping through expansion, and framed output can include nested properties.

## Sinatra/Rack support
JSON-LD 1.1 describes support for the _profile_ parameter to a media type in an HTTP ACCEPT header. This allows an HTTP request to specify the format (expanded/compacted/flattened/framed) along with a reference to a context or frame to use to format the returned document.

An HTTP header may be constructed as follows:

    GET /ordinary-json-document.json HTTP/1.1
    Host: example.com
    Accept: application/ld+json;profile="http://www.w3.org/ns/json-ld#compacted http://conneg.example.com/context", application/ld+json

This tells a server that the top priority is to return JSON-LD compacted using a context at `http://conneg.example.com/context`, and if not available, to just return any form of JSON-LD.

The {JSON::LD::ContentNegotiation} class provides a [Rack][Rack] `call` method, and [Sinatra][Sinatra] `registered` class method to allow content-negotiation using such profile parameters. For example:

    #!/usr/bin/env rackup
    require 'sinatra/base'
    require 'json/ld'

    module My
      class Application < Sinatra::Base
        register JSON::LD::ContentNegotiation

        get '/hello' do
          [{
            "http://example.org/input": [{
              "@id": "http://example.com/g1",
              "@graph": [{
                "http://example.org/value": [{"@value": "x"}]
              }]
            }]
          }])
        end
      end
    end

    run My::Application

The {JSON::LD::ContentNegotiation#call} method looks for a result which includes an object, with an acceptable `Accept` header and formats the result as JSON-LD, considering the profile parameters. This can be tested using something like the following:

    $ rackup config.ru

    $ curl -iH 'Accept: application/ld+json;profile="http://www.w3.org/ns/json-ld#compacted http://conneg.example.com/context"' http://localhost:9292/hello

See [Rack::LinkedData][] to do the same thing with an RDF Graph or Dataset as the source, rather than Ruby objects.

## Documentation
Full documentation available on [RubyDoc](https://ruby-rdf.github.io/json-ld/file/README.md)

## Differences from [JSON-LD API][]
The specified JSON-LD API is based on a WebIDL definition implementing [Promises][] intended for use within a browser.
This version implements a more Ruby-like variation of this API without the use
of promises or callback arguments, preferring Ruby blocks. All API methods
execute synchronously, so that the return from a method can typically be used as well as a block.

Note, the API method signatures differed in versions before 1.0, in that they also had a callback parameter. And 1.0.6 has some other minor method signature differences than previous versions. This should be the only exception to the use of semantic versioning.

### Principal Classes
* {JSON::LD}
  * {JSON::LD::API}
  * {JSON::LD::Compact}
  * {JSON::LD::Context}
  * {JSON::LD::Format}
  * {JSON::LD::Frame}
  * {JSON::LD::FromRDF}
  * {JSON::LD::Reader}
  * {JSON::LD::ToRDF}
  * {JSON::LD::Writer}

## Dependencies
* [Ruby](https://ruby-lang.org/) (>= 2.6)
* [RDF.rb](https://rubygems.org/gems/rdf) (~> 3.2)
* [JSON](https://rubygems.org/gems/json) (>= 2.6)

## Installation
The recommended installation method is via [RubyGems](https://rubygems.org/).
To install the latest official release of the `JSON-LD` gem, do:

    % [sudo] gem install json-ld

## Download
To get a local working copy of the development repository, do:

    % git clone git://github.com/ruby-rdf/json-ld.git

## Mailing List
* <https://lists.w3.org/Archives/Public/public-rdf-ruby/>

## Author
* [Gregg Kellogg](https://github.com/gkellogg) - <https://greggkellogg.net/>

## Contributing
* Do your best to adhere to the existing coding conventions and idioms.
* Don't use hard tabs, and don't leave trailing whitespace on any line.
* Do document every method you add using [YARD][] annotations. Read the
  [tutorial][YARD-GS] or just look at the existing code for examples.
* Don't touch the `json-ld.gemspec`, `VERSION` or `AUTHORS` files. If you need to
  change them, do so on your private branch only.
* Do feel free to add yourself to the `CREDITS` file and the corresponding
  list in the the `README`. Alphabetical order applies.
* Do note that in order for us to merge any non-trivial changes (as a rule
  of thumb, additions larger than about 15 lines of code), we need an
  explicit [public domain dedication][PDD] on record from you,
  which you will be asked to agree to on the first commit to a repo within the organization.
  Note that the agreement applies to all repos in the [Ruby RDF](https://github.com/ruby-rdf/) organization.

## License

This is free and unencumbered public domain software. For more information,
see <https://unlicense.org/> or the accompanying {file:UNLICENSE} file.

[Ruby]:             https://ruby-lang.org/
[RDF]:              https://www.w3.org/RDF/
[YARD]:             https://yardoc.org/
[YARD-GS]:          https://rubydoc.info/docs/yard/file/docs/GettingStarted.md
[PDD]:              https://unlicense.org/#unlicensing-contributions
[RDF.rb]:           https://rubygems.org/gems/rdf
[JSON-LD-star]:             https://json-ld.github.io/json-ld-star/
[Rack::LinkedData]: https://rubygems.org/gems/rack-linkeddata
[Backports]:        https://rubygems.org/gems/backports
[JSON-LD]:          https://www.w3.org/TR/json-ld11/ "JSON-LD 1.1"
[JSON-LD API]:      https://www.w3.org/TR/json-ld11-api/ "JSON-LD 1.1 Processing Algorithms and API"
[JSON-LD Framing]:  https://www.w3.org/TR/json-ld11-framing/ "JSON-LD 1.1 Framing"
[Promises]:         https://dom.spec.whatwg.org/#promises
[jsonlint]:         https://rubygems.org/gems/jsonlint
[Sinatra]:          https://www.sinatrarb.com/
[Rack]:             https://rack.github.com/

repo_name: ruby-rdf/json-ld

repo_language: HTML

mime_type: text/plain


text:
name: Build & deploy documentation
on:
  push:
    branches:
      - master
  workflow_dispatch:
jobs:
  build:
    runs-on: ubuntu-latest
    name: Update gh-pages with docs
    steps:
      - name: Clone repository
        uses: actions/checkout@v3
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.1"
      - name: Install required gem dependencies
        run: gem install yard --no-document
      - name: Build YARD Ruby Documentation
        run: yardoc
      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./doc/yard
          publish_branch: gh-pages


text:
# This workflow runs continuous CI across different versions of ruby on all branches and pull requests to develop.

name: CI
on:
  push:
    branches: [ '**' ]
  pull_request:
    branches: [ develop ]
  workflow_dispatch:

jobs:
  tests:
    name: Ruby ${{ matrix.ruby }}
    if: "contains(github.event.commits[0].message, '[ci skip]') == false"
    runs-on: ubuntu-latest
    env:
      CI: true
      ALLOW_FAILURES: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'jruby' }}
    strategy:
      fail-fast: false
      matrix:
        ruby: [2.6, 2.7, '3.0', 3.1, 3.2, ruby-head, jruby]
    steps:
      - name: Clone repository
        uses: actions/checkout@v3
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      - name: Install dependencies
        run: bundle install --jobs 4 --retry 3
      - name: Run tests
        run: ruby --version; bundle exec rspec spec || $ALLOW_FAILURES
      - name: Coveralls GitHub Action
        uses: coverallsapp/github-action@v2
        if: "matrix.ruby == '3.0'"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
  wintests:
    name: Win64 Ruby ${{ matrix.ruby }}
    if: "contains(github.event.commits[0].message, '[ci skip]') == false"
    runs-on: windows-latest
    env:
      CI: true
      ALLOW_FAILURES: ${{ matrix.ruby == '3.2' || matrix.ruby == 'jruby' }}
    strategy:
      fail-fast: false
      matrix:
        ruby: [3.1, 3.2]
    steps:
      - name: Clone repository
        uses: actions/checkout@v3
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      - name: Install dependencies
        run: bundle install --jobs 4 --retry 3
      - name: Run tests
        run: ruby --version; bundle exec rspec spec || $ALLOW_FAILURES
      - name: Coveralls GitHub Action
        uses: coverallsapp/github-action@v2
        if: "matrix.ruby == '3.0'"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}


text:
{
  "@context": "http://example.org/context.jsonld",
  "@id": "http://www.markus-lanthaler.com/",
  "@type": "Person",
  "name": "Markus Lanthaler",
  "knows" "http://greggkellogg.net/foaf#me"
}


text:
{
  "@context": "http://example.org/context.jsonld",
  "@id": "http://greggkellogg.net/foaf#me",
  "@type": "Person",
  "name": "Gregg Kellogg",
  "knows": {
    "@id": "http://www.markus-lanthaler.com/",
    "@type": "Person",
    "name": "Markus Lanthaler",
    "knows" "http://greggkellogg.net/foaf#me"
  }
}


text:
#!/usr/bin/env ruby
require 'rubygems'
$:.unshift(File.expand_path("../../lib", __FILE__))
require "bundler/setup"
require 'logger'
require 'json/ld'
require 'linkeddata'
require 'rdf/isomorphic'
require File.expand_path("../../spec/spec_helper", __FILE__)
require File.expand_path("../../spec/suite_helper", __FILE__)
require 'getoptlong'

ASSERTOR = "http://greggkellogg.net/foaf#me"
RUN_TIME = Time.now

MultiJson.use(:json_gem) # because of differences in parsing with OJ

def earl_preamble(options)
  options[:output].write File.read(File.expand_path("../../etc/doap.ttl", __FILE__))
  options[:output].puts %(
<https://rubygems.org/gems/json-ld> doap:release [
  doap:name "json-ld-#{JSON::LD::VERSION}";
  doap:revision "#{JSON::LD::VERSION}";
  doap:created "#{File.mtime(File.expand_path('../../VERSION', __FILE__)).strftime('%Y-%m-%d')}"^^xsd:date;
] .
<> foaf:primaryTopic <https://rubygems.org/gems/json-ld>;
  dc:issued "#{RUN_TIME.xmlschema}"^^xsd:dateTime;
  foaf:maker <#{ASSERTOR}> .

<#{ASSERTOR}> a earl:Assertor;
  foaf:title "Implementor" .
)
end

def compare_results(tc, result, expected)
  if tc.evaluationTest?
    if tc.testType == "jld:ToRDFTest"
      expected.equivalent_graph?(result) ? "passed": "failed"
    elsif tc.options[:ordered]
      expected == result ? "passed": "failed"
    else
      if !expected.equivalent_jsonld?(result)
        "failed"
      elsif result.to_s.include?('@context')
        exp_expected = JSON::LD::API.expand(expected, **tc.options.merge(logger: false))
        exp_result = JSON::LD::API.expand(result, **tc.options.merge(logger: false))
        exp_expected.equivalent_jsonld?(exp_result) ? "passed" : "failed"
      else
        "passed"
      end
    end
  else
    result.nil? ? "failed": "passed"
  end
end

def run_tc(man, tc, options)
  tc.options[:logger] = options[:logger]
  tc.options[:documentLoader] ||= Fixtures::SuiteTest.method(:documentLoader)
  tc.options[:lowercaseLanguage] = true

  if tc.options[:specVersion] == "json-ld-1.0"
    STDERR.puts "skip #{tc.property('input')} (1.0 test)" if options[:verbose]
    return
  end

  STDERR.write "run #{tc.property('input')}"

  if options[:verbose]
    puts "\nTestCase: #{tc.inspect}"
    puts "\nInput:\n" + tc.input
    puts "\nContext:\n" + tc.context if tc.context
    puts "\nFrame:\n" + tc.frame if tc.frame
    puts "\nExpected:\n" + tc.expect if tc.expect && tc.positiveTest?
    puts "\nExpected:\n" + tc.expectErrorCode if tc.negativeTest?
  end

  output = ""
  begin
    puts "open #{tc.input_loc}" if options[:verbose]

    result = case tc.testType
    when 'jld:CompactTest'
      output = JSON::LD::API.compact(tc.input_loc, tc.context_json['@context'], validate: true, **tc.options)
      expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest?
      compare_results(tc, output, expected)
    when 'jld:ExpandTest'
      # MultiJson use OJ, by default, which doesn't handle native numbers the same as the JSON gem.
      output = JSON::LD::API.expand(tc.input_loc, validate: true, **tc.options)
      expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest?
      compare_results(tc, output, expected)
    when 'jld:FlattenTest'
      output = JSON::LD::API.flatten(tc.input_loc, (tc.context_json['@context'] if tc.context_loc), validate: true, **tc.options)
      expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest?
      compare_results(tc, output, expected)
    when 'jld:FrameTest'
      output = JSON::LD::API.frame(tc.input_loc, tc.frame_loc, validate: true, **tc.options)
      expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest?
      compare_results(tc, output, expected)
    when 'jld:FromRDFTest'
      repo = RDF::Repository.load(tc.input_loc, format: :nquads, rdfstar: tc.options[:rdfstar])
      output = if options[:stream]
        JSON.parse(JSON::LD::Writer.buffer(stream: true, validate: true, **tc.options) {|w| w << repo})
      else
        JSON::LD::API.fromRdf(repo, validate: true, **tc.options)
      end
      expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest?
      compare_results(tc, output, expected)
    when 'jld:ToRDFTest'
      output = RDF::Repository.new.extend(RDF::Isomorphic)
      if options[:stream]
        JSON::LD::Reader.open(tc.input_loc, stream: true, **tc.options.merge(logger: false)) do |statement|
          output << statement
        end
      else
        JSON::LD::API.toRdf(tc.input_loc, **tc.options).map do |statement|
          output << statement
        end
      end

      if tc.evaluationTest? && tc.positiveTest?
        begin
          if tc.options[:produceGeneralizedRdf]
            quads = JSON::LD::API.toRdf(tc.input_loc, **tc.options.merge(validate: false)).map do |statement|
              # Not really RDF, try different test method
              tc.to_quad(statement)
            end
            output = quads.sort.uniq.join("")
            # toRdf/e075 is hard to test, but verified manually
            output == tc.expect ? 'passed' : (tc.input_loc.include?('e075') ? 'passed' : 'failed')
          else
            expected = RDF::Repository.new << RDF::NQuads::Reader.new(tc.expect, rdfstar: tc.options[:rdfstar], validate: false, logger: [])
            output.isomorphic?(expected) ? 'passed' : 'failed'
          end
        rescue RDF::ReaderError, JSON::LD::JsonLdError
          quads = JSON::LD::API.toRdf(tc.input_loc, rdfstar: tc.options[:rdfstar], **tc.options.merge(validate: false)).map do |statement|
            # Not really RDF, try different test method
            tc.to_quad(statement)
          end

          # FIXME: toRDF is outputing duplicate quads
          output = quads.sort.uniq.join("")
          output == tc.expect ? 'passed' : 'failed'
        end
      else
        output.count > 0 ? 'passed' : 'failed'
      end
    end || "untested"

    output = output.dump(:nquads, validate: false) rescue output.to_s if output.is_a?(RDF::Enumerable)
    puts "\nOutput:\n" + (tc.testType == 'jld:ToRDFTest' ? output : output.to_json(JSON::LD::JSON_STATE)) if !tc.syntaxTest? && options[:verbose]

    result = result ? 'failed' : 'passed' unless tc.positiveTest?
    options[:results][result] ||= 0
    options[:results][result] += 1
  rescue Interrupt
    $stderr.puts "(interrupt)"
    exit 1
  rescue StandardError => e
    result = if tc.positiveTest?
      STDERR.puts "#{" exception" unless options[:quiet]}: #{e}"
      if options[:quiet] || !options[:verbose]
        options[:results]['failed'] ||= 0
        options[:results]['failed'] += 1
      else
        raise
      end
      "failed"
    else
      if e.message.include?(tc.property('expectErrorCode'))
        options[:results]['passed'] ||= 0
        options[:results]['passed'] += 1
        "passed"
      else
        STDERR.puts("Expected exception: '#{tc.property('expectErrorCode')}' not '#{e}'") unless options[:quiet]
        options[:results]['failed'] ||= 0
        options[:results]['failed'] += 1
        "failed"
      end
    end
  end

  #options[:output].puts("\nOutput:\n" + output) unless options[:quiet]

  if options[:earl]
    options[:output].puts %{
[ a earl:Assertion;
  earl:assertedBy <#{ASSERTOR}>;
  earl:subject <https://rubygems.org/gems/json-ld>;
  earl:test <#{man}#{tc.id}>;
  earl:result [
    a earl:TestResult;
    earl:outcome earl:#{result};
    dc:date "#{RUN_TIME.xmlschema}"^^xsd:dateTime];
  earl:mode earl:automatic ] .
}
  end

  puts "#{" test result:" unless options[:quiet]} #{result}"
end

logger = Logger.new(STDERR)
logger.level = Logger::WARN
logger.formatter = lambda {|severity, datetime, progname, msg| "#{severity}: #{msg}\n"}

options = {
  output: STDOUT,
  results: {},
  logger: logger
}

opts = GetoptLong.new(
  ["--help", "-?", GetoptLong::NO_ARGUMENT],
  ["--debug", GetoptLong::NO_ARGUMENT],
  ["--earl", GetoptLong::NO_ARGUMENT],
  ["--quiet", "-q", GetoptLong::NO_ARGUMENT],
  ["--output", "-o", GetoptLong::REQUIRED_ARGUMENT],
  ["--stream",   GetoptLong::NO_ARGUMENT],
  ["--verbose", "-v", GetoptLong::NO_ARGUMENT]
)

def help(options)
  puts "Usage: #{$0} [options] [test-number ...]"
  puts "Options:"
  puts "      --debug:              Display detailed debug output"
  puts "      --earl:               Generate EARL report"
  puts "      --quiet:              Minimal output"
  puts "      --output:             Output to specified file"
  puts "      --stream:             Use streaming RDF reader/writer"
  puts "      --verbose:            Verbose processing"
  puts "      --help,-?:            This message"
  exit(0)
end

opts.each do |opt, arg|
  case opt
  when '--help'             then help(options)
  when '--debug'            then logger.level = Logger::DEBUG
  when '--earl'
    options[:quiet] = options[:earl] = true
    logger.level = Logger::FATAL
  when '--output'           then options[:output] = File.open(arg, "w")
  when '--quiet'
    options[:quiet] = true
    logger.level = Logger::FATAL
  when '--stream'           then options[:stream] = true
  when '--verbose'          then options[:verbose] = true
  end
end

manifests = if options[:stream]
  ["#{Fixtures::SuiteTest::STREAM_SUITE}stream-toRdf-manifest.jsonld"]
else
  %w(expand compact flatten fromRdf html remote-doc toRdf).map do |man|
    "#{Fixtures::SuiteTest::SUITE}#{man}-manifest.jsonld"
  end +
  ["#{Fixtures::SuiteTest::FRAME_SUITE}frame-manifest.jsonld"] +
  %w{expand compact flatten fromRdf toRdf}.map do |man|
    "#{Fixtures::SuiteTest::STAR_SUITE}#{man}-manifest.jsonld"
  end
end

earl_preamble(options) if options[:earl]

manifests.each do |man|
  Fixtures::SuiteTest::Manifest.open(man) do |m|
    m.entries.each do |tc|
      next unless ARGV.empty? || ARGV.any? {|n| tc.property('@id').match(/#{n}/) || tc.property('input').match(/#{n}/)}
      options = {stream: true}.merge(options) if man.include?('stream')
      run_tc(man.sub(".jsonld", ""), tc, options)
    end
  end
end

options[:results].each {|k, v| puts "#{k}: #{v}"}


text:
#!/usr/bin/env ruby
# Generate a context for schema.org from the RDFS definition.
# Include the RDFS definition recursed
require 'rubygems'
require "bundler/setup"
$:.unshift(File.expand_path("../../lib", __FILE__))
begin
  require 'linkeddata'
rescue LoadError
end
require 'json/ld'
require 'getoptlong'
require 'rdf/rdfa'
require 'json/ld'

options = {
  output: STDOUT,
  prefixes: {}
}

OPT_ARGS = [
  ["--body",          GetoptLong::NO_ARGUMENT,      "Include the vocabulary definition in the body of the document"],
  ["--hierarchical",  GetoptLong::NO_ARGUMENT,      "Create a hierachy using 'children' as reverse of rdfs:subClassOf"],
  ["--initial",       GetoptLong::REQUIRED_ARGUMENT,"Initial context to load, overrides default"],
  ["--language",      GetoptLong::REQUIRED_ARGUMENT,"Default language for vocabulary"],
  ["--output", "-o",  GetoptLong::REQUIRED_ARGUMENT,"Output to the specified file path"],
  ["--prefix",        GetoptLong::REQUIRED_ARGUMENT,"space-separated prefix uri combination"],
  ["--quiet",         GetoptLong::NO_ARGUMENT,      "Supress most output other than progress indicators"],
  ["--verbose",       GetoptLong::NO_ARGUMENT,      "Detail on execution"],
  ["--vocab",         GetoptLong::REQUIRED_ARGUMENT,"Set context @vocab to specified URI"],
  ["--help", "-?",    GetoptLong::NO_ARGUMENT,      "This message"]
]
def usage
  STDERR.puts %{Usage: #{$0} [options] URL ...}
  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

base_context = ::JSON.parse %({
  "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
  "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
  "rdfs:domain": {"@type": "@id"},
  "rdfs:range": {"@type": "@id"},
  "rdfs:subClassOf": {"@type": "@id"},
  "rdfs:subPropertyOf": {"@type": "@id"},
  "owl": "http://www.w3.org/2002/07/owl#",
  "owl:equivalentClass": {"@type": "@vocab"},
  "owl:equivalentProperty": {"@type": "@vocab"},
  "owl:oneOf": {"@container": "@list", "@type": "@vocab"},
  "owl:imports": {"@type": "@id"},
  "owl:versionInfo": {"@type": "xsd:string", "@language": null},
  "owl:inverseOf": {"@type": "@vocab"}
})

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

opts.each do |opt, arg|
  case opt
  when '--body'         then options[:include_body] = true
  when '--initial'      then base_context = ::JSON.parse(File.read(arg))
  when '--hierarchical' then options[:hier] = true
  when '--language'     then options[:language] = arg
  when '--output'       then options[:output] = File.open(arg, "w")
  when '--quiet'        then options[:quiet] = true
  when '--prefix'       then pfx,uri = arg.split(' '); options[:prefixes][pfx.to_sym] = uri
  when '--vocab'        then options[:vocab] = arg
  when '--help'         then usage
  end
end

# Load vocabulary
graph = RDF::Graph.load(ARGV[0])

context = JSON::LD::Context.new(**options).
  parse(base_context).
  from_vocabulary(graph)

# Simply serialize with the generated context
if options[:hier]
  STDERR.puts "Hierarchical implies adding body" unless options[:include_body]

  class_map = {}
  property_map = {}
  json = JSON::LD::API.fromRDF(graph) do |expanded|
    JSON::LD::API.compact(expanded, context) do |compacted|
      compacted['@graph'].each do |obj|
        #next if type_exclusion.include?(obj['name'])
        if obj['@type'].include?("rdfs:Class")
          class_map[obj['@id']] = obj
        elsif obj['@type'].include?("rdf:Property")
          property_map[obj['@id']] = obj
        end
      end
    end
  end

  thing = class_map['schema:Thing']
  datatype = class_map['schema:DataType']

  # Build class hierarchy
  class_map.each do |id, obj|
    [obj['rdfs:subClassOf']].flatten.compact.each do |super_class|
      so = class_map[super_class]
      #next if so.nil?
      raise "super class not found: #{super_class}" if so.nil?
      (so['children'] ||= []) << obj
    end
  end

  # Add properties to classes
  property_map.each do |id, obj|
    [obj['domainIncludes']].flatten.compact.each do |domain|
      dom = class_map[domain]
      #next if dom.nil?
      raise "domain not found: #{domain}" if dom.nil?
      (dom['properties'] ||= []) << obj
    end
  end

  result = {
    "@context" => [context.serialize['@context'], {
      "children" => {"@reverse" => "rdfs:subClassOf"},
      "properties" => {"@reverse" => "schema:domainIncludes"}
      }],
    "@graph" => [thing, datatype]
  }

  options[:output].puts(result.to_json(JSON::LD::JSON_STATE))
elsif options[:include_body]
  JSON::LD::Writer.new(options[:output], context: context, standard_prefixes: true) {|w| w << graph}
else
  options[:output].puts(context.serialize.to_json(JSON::LD::JSON_STATE))
end

#object_map = {}
#
## Extract IDs
#RDF::Repository.load("http://schema.org/docs/schema_org_rdfa.html") do |repo|
#  JSON::LD::API.fromRDF(repo) do |expanded|
#    JSON::LD::API.compact(expanded, context) do |compacted|
#      compacted['@graph'].each do |obj|
#        next if type_exclusion.include?(obj['name'])
#        object_map[obj['@id']] = obj if obj['@type'] == "rdfs:Class"
#      end
#    end
#  end
#end
#
#thing = object_map['schema:Thing']
#
## Build type hierarchy
#object_map.each do |id, obj|
#  [obj['rdfs:subClassOf']].flatten.compact.each do |super_class|
#    so = object_map[super_class]
#    next if so.nil?
#    raise "super class not found: #{super_class}" if so.nil?
#    (so['children'] ||= []) << obj
#  end
#end
#
#context = context.merge({"children" => {"@reverse" => "rdfs:subClassOf"}})
#context = {"@context" => context}
#puts context.merge(thing).to_json(JSON::LD::JSON_STATE)


text:
#!/usr/bin/env ruby
require 'rubygems'
require "bundler/setup"
$:.unshift(File.expand_path("../../lib", __FILE__))
require 'rdf/turtle'
begin
  require 'linkeddata'
rescue LoadError
end
require 'logger'
require 'json/ld'
require 'getoptlong'
require 'open-uri'
require 'ruby-prof'

def run(input, options)
  if options[:profile]
    output_dir = File.expand_path("../../doc/profiles/#{File.basename __FILE__, ".rb"}", __FILE__)
    FileUtils.mkdir_p(output_dir)
    profile = RubyProf::Profile.new
    profile.exclude_methods!(Array, :each, :map)
    profile.exclude_method!(Hash, :each)
    profile.exclude_method!(Kernel, :require)
    profile.exclude_method!(Object, :run)
    profile.exclude_common_methods!
    profile.start
    run(input, **options.merge(profile: false))
    result = profile.stop

    # Print a graph profile to text
    printer = RubyProf::MultiPrinter.new(result)
    printer.print(path: output_dir, profile: "profile")
    puts "output saved in #{output_dir}"
    return
  end
  reader_class = RDF::Reader.for(options[:input_format].to_sym)
  raise "Reader not found for #{options[:input_format]}" unless reader_class

  start = Time.new
  if options[:flatten]
    output = JSON::LD::API.flatten(input, options.delete(:context), **options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    puts "Flattened in #{secs} seconds."
  elsif options[:expand]
    options = options.merge(expandContext: options.delete(:context)) if options.key?(:context)
    output = JSON::LD::API.expand(input, **options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    puts "Expanded in #{secs} seconds."
  elsif options[:compact]
    output = JSON::LD::API.compact(input, options[:context], **options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    puts "Compacted in #{secs} seconds."
  elsif options[:frame]
    output = JSON::LD::API.frame(input, options[:frame], **options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    puts "Framed in #{secs} seconds."
  else
    r = reader_class.new(input, **options[:parser_options])
    if options[:output_format] == :none
      num = 0
      r.each_statement { num += 1 } # Eat statements
      secs = Time.new - start
    else
      g = RDF::Repository.new << r
      secs = Time.new - start
      num = g.count
      parser_options = options[:parser_options].merge(prefixes: r.prefixes, standard_prefixes: true)
      options[:output].puts g.dump(options[:output_format], **parser_options)
      puts
    end
    puts "Parsed #{num} statements in #{secs} seconds @ #{num/secs} statements/second."
  end
rescue
  fname = input.respond_to?(:path) ? input.path : "-stdin-"
  STDERR.puts("Error in #{fname}")
  raise
end

logger = Logger.new(STDERR)
logger.level = Logger::WARN
logger.formatter = lambda {|severity, datetime, progname, msg| "#{severity}: #{msg}\n"}

parser_options = {
  base:     nil,
  progress: false,
  profile:  false,
  validate: false,
  logger:   logger,
}

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

OPT_ARGS = [
  ["--debug", GetoptLong::NO_ARGUMENT,                "Debug output"],
  ["--compact", GetoptLong::NO_ARGUMENT,              "Compact input, using context"],
  ["--context", GetoptLong::REQUIRED_ARGUMENT,        "Context used for compaction"],
  ["--execute", "-e", GetoptLong::REQUIRED_ARGUMENT,  "Use option argument as the patch input"],
  ["--expand", GetoptLong::NO_ARGUMENT,               "Expand input"],
  ["--expanded", GetoptLong::NO_ARGUMENT,             "Input is already expanded"],
  ["--flatten", GetoptLong::NO_ARGUMENT,              "Flatten input"],
  ["--format", GetoptLong::REQUIRED_ARGUMENT,         "Output format, for RDF output"],
  ["--frame", GetoptLong::REQUIRED_ARGUMENT,          "Frame input, option value is frame to use"],
  ["--help", "-?", GetoptLong::NO_ARGUMENT,           "This message"],
  ["--input-format", GetoptLong::REQUIRED_ARGUMENT,   "Format of input, if not JSON-LD"],
  ["--output", "-o", GetoptLong::REQUIRED_ARGUMENT,   "Where to store output (default STDOUT)"],
  ["--profile", GetoptLong::NO_ARGUMENT,              "Run profiler with output to doc/profiles/"],
  ["--quiet", GetoptLong::NO_ARGUMENT,                "Reduce output"],
  ["--rdfstar", GetoptLong::NO_ARGUMENT,              "RDF-star mode"],
  ["--stream", GetoptLong::NO_ARGUMENT,               "Streaming reader/writer"],
  ["--uri", GetoptLong::REQUIRED_ARGUMENT,            "Run with argument value as base"],
  ["--validate", GetoptLong::NO_ARGUMENT,             "Validate input"],
]

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

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.each do |opt, arg|
  case opt
  when '--debug'        then logger.level = Logger::DEBUG
  when '--compact'      then options[:compact] = true
  when '--context'      then options[:context] = arg
  when '--execute'      then input = arg
  when '--expand'       then options[:expand] = true
  when '--expanded'     then options[:expanded] = true
  when '--flatten'      then options[:flatten] = true
  when '--format'       then options[:output_format] = arg.to_sym
  when '--frame'        then options[:frame] = arg
  when "--help"         then usage
  when '--input-format' then options[:input_format] = arg.to_sym
  when '--output'       then options[:output] = File.open(arg, "w")
  when '--profile'      then options[:profile] = true
  when '--quiet'
    options[:quiet] = true
    logger.level = Logger::FATAL
  when '--rdfstar'      then parser_options[:rdfstar] = true
  when '--stream'       then parser_options[:stream] = true
  when '--uri'          then parser_options[:base] = arg
  when '--validate'     then parser_options[:validate] = true
  end
end

if ARGV.empty?
  s = input ? input : $stdin.read
  run(StringIO.new(s), options)
else
  ARGV.each do |test_file|
    io = Kernel.open(test_file)
    io.define_singleton_method(:content_type) {"text/html"} if test_file.end_with?('.html')
    run(io, options)
  end
end
puts


text:
#!/usr/bin/env ruby
require 'rubygems'
begin
  require 'linkeddata'
rescue LoadError
end
$:.unshift(File.expand_path("../../lib", __FILE__))
require 'json/ld'
require 'getoptlong'
require 'open-uri'
require 'logger'

def run(input, options, parser_options)
  reader_class = RDF::Reader.for(options[:input_format].to_sym)
  raise "Reader not found for #{options[:input_format]}" unless reader_class

  # Override default (or specified) output format when framing
  options[:format] = :jsonld if options[:compact] || options[:frame]

  # If input format is not JSON-LD, transform input to JSON-LD first
  reader = if options[:input_format] != :jsonld
    reader_class.new(input, parser_options)
  end

  start = Time.new
  if options[:expand]
    parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.key?(:context)
    input = JSON::LD::API.fromRdf(reader) if reader
    output = JSON::LD::API.expand(input, **parser_options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    STDERR.puts "Expanded in #{secs} seconds." unless options[:quiet]
  elsif options[:compact]
    input = JSON::LD::API.fromRdf(reader) if reader
    output = JSON::LD::API.compact(input, parser_options[:context], **parser_options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    STDERR.puts "Compacted in #{secs} seconds." unless options[:quiet]
  elsif options[:flatten]
    input = JSON::LD::API.fromRdf(reader) if reader
    output = JSON::LD::API.flatten(input, parser_options[:context], **parser_options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    STDERR.puts "Flattened in #{secs} seconds." unless options[:quiet]
  elsif options[:frame]
    input = JSON::LD::API.fromRdf(reader) if reader
    output = JSON::LD::API.frame(input, parser_options[:frame], **parser_options)
    secs = Time.new - start
    options[:output].puts output.to_json(JSON::LD::JSON_STATE)
    STDERR.puts "Framed in #{secs} seconds." unless options[:quiet]
  else
    parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.key?(:context)
    parser_options[:standard_prefixes] = true
    reader ||= JSON::LD::Reader.new(input, **parser_options)
    num = 0
    RDF::Writer.for(options[:output_format]).new(options[:output], **parser_options) do |w|
      reader.each do |statement|
        num += 1
        w << statement
      end
    end
    secs = Time.new - start
    STDERR.puts "\nParsed #{num} statements in #{secs} seconds @ #{num/secs} statements/second." unless options[:quiet]
  end
rescue
  fname = case
  when input.respond_to?(:path) then input.path
  when input.respond_to?(:requested_url) && input.requested_url then input.requested_url
  when input.respond_to?(:base_uri) then input.base_uri
  else  "-stdin-"
  end
  STDERR.puts("Error in #{fname}")
  raise
end

logger = Logger.new(STDERR)
logger.level = Logger::WARN
logger.formatter = lambda {|severity, datetime, progname, msg| "#{severity}: #{msg}\n"}

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

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

OPT_ARGS = [
  ["--debug",         GetoptLong::NO_ARGUMENT,      "Turn on verbose debugging"],
  ["--compact",       GetoptLong::NO_ARGUMENT,      "Compact document, using --context"],
  ["--compactArrays", GetoptLong::OPTIONAL_ARGUMENT, "Set compactArrays option"],
  ["--context",       GetoptLong::REQUIRED_ARGUMENT,"Context to apply for expand, compact and converting from RDF"],
  ["--embed",         GetoptLong::REQUIRED_ARGUMENT,"a flag specifying that objects should be directly embedded in the output, instead of being referred to by their IRI"],
  ["--evaluate","-e", GetoptLong::REQUIRED_ARGUMENT,"Evaluate argument as a JSON-LD document"],
  ["--expand",        GetoptLong::NO_ARGUMENT,      "Expand document, using an optional --context"],
  ["--expanded",      GetoptLong::OPTIONAL_ARGUMENT, "Input is already expanded"],
  ["--explicit",      GetoptLong::OPTIONAL_ARGUMENT, "a flag specifying that for properties to be included in the output, they must be explicitly declared in the framing context"],
  ["--flatten",       GetoptLong::NO_ARGUMENT,      "Flatten document, using an optional --context"],
  ["--format",        GetoptLong::REQUIRED_ARGUMENT,"Specify output format when converting to RDF"],
  ["--frame",         GetoptLong::REQUIRED_ARGUMENT,"Frame document, using the file or URL as a frame specification"],
  ["--input-format",  GetoptLong::REQUIRED_ARGUMENT,"Format of the input document, when converting from RDF."],
  ["--omitDefault",   GetoptLong::OPTIONAL_ARGUMENT,"a flag specifying that properties that are missing from the JSON-LD input should be omitted from the output"],
  ["--output", "-o",  GetoptLong::REQUIRED_ARGUMENT,"Output to the specified file path"],
  ["--parse-only",    GetoptLong::NO_ARGUMENT,      "Parse the document for well-formedness only"],
  ["--processingMode",GetoptLong::REQUIRED_ARGUMENT,"Set processing mode, defaults to json-ld-1.1"],
  ["--quiet",         GetoptLong::NO_ARGUMENT,      "Supress most output other than progress indicators"],
  ["--rename_bnodes", GetoptLong::OPTIONAL_ARGUMENT,"Rename bnodes as part of expansion, or keep them the same"],
  ["--rdfstar",       GetoptLong::NO_ARGUMENT,      "Parse JSON-LD-star"],
  ["--requireAll",    GetoptLong::OPTIONAL_ARGUMENT,"Rename bnodes as part of expansion, or keep them the same"],
  ["--stream",        GetoptLong::NO_ARGUMENT,      "Use Streaming reader/writer"],
  ["--unique_bnodes", GetoptLong::OPTIONAL_ARGUMENT,"Use unique bnode identifiers"],
  ["--uri",           GetoptLong::REQUIRED_ARGUMENT,"URI to be used as the document base"],
  ["--validate",      GetoptLong::NO_ARGUMENT,      "Validate while processing"],
  ["--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 '--debug'          then logger.level = Logger::DEBUG
  when '--compact'        then options[:compact] = true
  when "--compactArrays"  then parser_options[:compactArrays] = (arg || 'true') == 'true'
  when '--context'        then parser_options[:context] = RDF::URI(arg).absolute? ? arg : File.open(arg)
  when '--evaluate'       then input = arg
  when '--expand'         then options[:expand] = true
  when "--expanded"       then parser_options[:expanded] = (arg || 'true') == 'true'
  when "--explicit"       then parser_options[:compactArrays] = (arg || 'true') == 'true'
  when '--format'         then options[:output_format] = arg.to_sym
  when '--flatten'        then options[:flatten] = arg
  when '--frame'          then options[:frame] = parser_otpions[:frame] = RDF::URI(arg).absolute? ? arg : File.open(arg)
  when '--input-format'   then options[:input_format] = arg.to_sym
  when "--omitDefault"    then parser_options[:omitDefault] = (arg || 'true') == 'true'
  when '--output'         then options[:output] = File.open(arg, "w")
  when '--parse-only'     then options[:parse_only] = true
  when '--processingMode' then parser_options[:processingMode] = arg
  when '--quiet'
    options[:quiet] = true
    logger.level = Logger::FATAL
  when "--rdfstar"        then parser_options[:rdfstar] = true
  when "--rename_bnodes"  then parser_options[:rename_bnodes] = (arg || 'true') == 'true'
  when "--requireAll"     then parser_options[:requireAll] = (arg || 'true') == 'true'
  when '--stream'         then parser_options[:stream] = true
  when "--unique_bnodes"  then parser_options[:unique_bnodes] = (arg || 'true') == 'true'
  when '--uri'            then parser_options[:base] = arg
  when '--validate'       then parser_options[:validate] = true
  when '--help'           then usage
  when '--embed'
    case arg
    when '@always', '@never', '@link', '@once'
      parser_options[:embed] = arg
    when 'true'
      parser_options[:embed] = '@never'
    when 'false'
      parser_options[:embed] = '@first'
    else
      STDERR.puts "--embed option takes one of @always, @never, @link, or @once"
      exit(1)
    end
  end
end

# Hack
if !(options.keys & %i{expand compact flatten frame}).empty? &&
   (parser_options[:stream] || options[:output_format] != :jsonld)
   STDERR.puts "Incompatible options"
   exit(1)
end

if ARGV.empty?
  s = input ? input : $stdin.read
  run(StringIO.new(s), options, parser_options)
else
  ARGV.each do |file|
    # Call with opened files
    RDF::Util::File.open_file(file, **options) {|f| run(f, options, parser_options)}
  end
end
puts


text:
# frozen_string_literal: true

$LOAD_PATH.unshift(File.expand_path('ld', __dir__))
require 'rdf' # @see https://rubygems.org/gems/rdf
require 'multi_json'
require 'set'

module JSON
  ##
  # **`JSON::LD`** is a JSON-LD extension for RDF.rb.
  #
  # @example Requiring the `JSON::LD` module
  #   require 'json/ld'
  #
  # @example Parsing RDF statements from a JSON-LD file
  #   JSON::LD::Reader.open("etc/foaf.jld") do |reader|
  #     reader.each_statement do |statement|
  #       puts statement.inspect
  #     end
  #   end
  #
  # @see https://rubygems.org/gems/rdf
  # @see http://www.w3.org/TR/REC-rdf-syntax/
  #
  # @author [Gregg Kellogg](http://greggkellogg.net/)
  module LD
    require 'json'
    require 'json/ld/extensions'
    require 'json/ld/format'
    require 'json/ld/utils'
    autoload :API,                'json/ld/api'
    autoload :ContentNegotiation, 'json/ld/conneg'
    autoload :Context,            'json/ld/context'
    autoload :Normalize,          'json/ld/normalize'
    autoload :Reader,             'json/ld/reader'
    autoload :Resource,           'json/ld/resource'
    autoload :StreamingReader,    'json/ld/streaming_reader'
    autoload :StreamingWriter,    'json/ld/streaming_writer'
    autoload :VERSION,            'json/ld/version'
    autoload :Writer,             'json/ld/writer'

    # JSON-LD profiles
    JSON_LD_NS = 'http://www.w3.org/ns/json-ld#'
    PROFILES = %w[expanded compacted flattened framed].map { |p| JSON_LD_NS + p }.freeze

    # Default context when compacting without one being specified
    DEFAULT_CONTEXT = 'http://schema.org'

    # Acceptable MultiJson adapters
    MUTLI_JSON_ADAPTERS = %i[oj json_gem json_pure ok_json yajl nsjsonseerialization]

    KEYWORDS = Set.new(%w[
                         @annotation
                         @base
                         @container
                         @context
                         @default
                         @direction
                         @embed
                         @explicit
                         @first
                         @graph
                         @id
                         @import
                         @included
                         @index
                         @json
                         @language
                         @list
                         @nest
                         @none
                         @omitDefault
                         @propagate
                         @protected
                         @preserve
                         @requireAll
                         @reverse
                         @set
                         @type
                         @value
                         @version
                         @vocab
                       ]).freeze

    # Regexp matching an NCName.
    NC_REGEXP = Regexp.new(
      %{^
        (?!\\\\u0301)             # ́ is a non-spacing acute accent.
                                  # It is legal within an XML Name, but not as the first character.
        (  [a-zA-Z_]
         | \\\\u[0-9a-fA-F]
        )
        (  [0-9a-zA-Z_.-]
         | \\\\u([0-9a-fA-F]{4})
        )*
      $},
      Regexp::EXTENDED
    )

    # Datatypes that are expressed in a native form and don't expand or compact
    NATIVE_DATATYPES = [RDF::XSD.integer.to_s, RDF::XSD.boolean.to_s, RDF::XSD.double.to_s]

    JSON_STATE = JSON::State.new(
      indent:       '  ',
      space:        ' ',
      space_before: '',
      object_nl:    "\n",
      array_nl:     "\n"
    )

    MAX_CONTEXTS_LOADED = 50

    # URI Constants
    RDF_JSON = RDF::URI("#{RDF.to_uri}JSON")
    RDF_DIRECTION = RDF::URI("#{RDF.to_uri}direction")
    RDF_LANGUAGE = RDF::URI("#{RDF.to_uri}language")

    class JsonLdError < StandardError
      def to_s
        "#{self.class.instance_variable_get :@code}: #{super}"
      end

      def code
        self.class.instance_variable_get :@code
      end

      class CollidingKeywords < JsonLdError; @code = 'colliding keywords'; end
      class ConflictingIndexes < JsonLdError; @code = 'conflicting indexes'; end
      class CyclicIRIMapping < JsonLdError; @code = 'cyclic IRI mapping'; end
      class InvalidAnnotation < JsonLdError; @code = 'invalid annotation'; end
      class InvalidBaseIRI < JsonLdError; @code = 'invalid base IRI'; end
      class InvalidContainerMapping < JsonLdError; @code = 'invalid container mapping'; end
      class InvalidContextEntry < JsonLdError; @code = 'invalid context entry'; end
      class InvalidContextNullification < JsonLdError; @code = 'invalid context nullification'; end
      class InvalidDefaultLanguage < JsonLdError; @code = 'invalid default language'; end
      class InvalidIdValue < JsonLdError; @code = 'invalid @id value'; end
      class InvalidIndexValue < JsonLdError; @code = 'invalid @index value'; end
      class InvalidVersionValue < JsonLdError; @code = 'invalid @version value'; end
      class InvalidImportValue < JsonLdError; @code = 'invalid @import value'; end
      class InvalidIncludedValue < JsonLdError; @code = 'invalid @included value'; end
      class InvalidIRIMapping < JsonLdError; @code = 'invalid IRI mapping'; end
      class InvalidJsonLiteral < JsonLdError; @code = 'invalid JSON literal'; end
      class InvalidKeywordAlias < JsonLdError; @code = 'invalid keyword alias'; end
      class InvalidLanguageMapping < JsonLdError; @code = 'invalid language mapping'; end
      class InvalidLanguageMapValue < JsonLdError; @code = 'invalid language map value'; end
      class InvalidLanguageTaggedString < JsonLdError; @code = 'invalid language-tagged string'; end
      class InvalidLanguageTaggedValue < JsonLdError; @code = 'invalid language-tagged value'; end
      class InvalidLocalContext < JsonLdError; @code = 'invalid local context'; end
      class InvalidNestValue < JsonLdError; @code = 'invalid @nest value'; end
      class InvalidPrefixValue < JsonLdError; @code = 'invalid @prefix value'; end
      class InvalidPropagateValue < JsonLdError; @code = 'invalid @propagate value'; end
      class InvalidEmbeddedNode < JsonLdError; @code = 'invalid embedded node'; end
      class InvalidRemoteContext < JsonLdError; @code = 'invalid remote context'; end
      class InvalidReverseProperty < JsonLdError; @code = 'invalid reverse property'; end
      class InvalidReversePropertyMap < JsonLdError; @code = 'invalid reverse property map'; end
      class InvalidReversePropertyValue < JsonLdError; @code = 'invalid reverse property value'; end
      class InvalidReverseValue < JsonLdError; @code = 'invalid @reverse value'; end
      class InvalidScopedContext < JsonLdError; @code = 'invalid scoped context'; end
      class InvalidScriptElement < JsonLdError; @code = 'invalid script element'; end
      class InvalidSetOrListObject < JsonLdError; @code = 'invalid set or list object'; end
      class InvalidStreamingKeyOrder < JsonLdError; @code = 'invalid streaming key order' end
      class InvalidTermDefinition < JsonLdError; @code = 'invalid term definition'; end
      class InvalidBaseDirection < JsonLdError; @code = 'invalid base direction'; end
      class InvalidTypedValue < JsonLdError; @code = 'invalid typed value'; end
      class InvalidTypeMapping < JsonLdError; @code = 'invalid type mapping'; end
      class InvalidTypeValue < JsonLdError; @code = 'invalid type value'; end
      class InvalidValueObject < JsonLdError; @code = 'invalid value object'; end
      class InvalidValueObjectValue < JsonLdError; @code = 'invalid value object value'; end
      class InvalidVocabMapping < JsonLdError; @code = 'invalid vocab mapping'; end
      class IRIConfusedWithPrefix < JsonLdError; @code = 'IRI confused with prefix'; end
      class KeywordRedefinition < JsonLdError; @code = 'keyword redefinition'; end
      class LoadingDocumentFailed < JsonLdError; @code = 'loading document failed'; end
      class LoadingRemoteContextFailed < JsonLdError; @code = 'loading remote context failed'; end
      class ContextOverflow < JsonLdError; @code = 'context overflow'; end
      class MissingIncludedReferent < JsonLdError; @code = 'missing @included referent'; end
      class MultipleContextLinkHeaders < JsonLdError; @code = 'multiple context link headers'; end
      class ProtectedTermRedefinition < JsonLdError; @code = 'protected term redefinition'; end
      class ProcessingModeConflict < JsonLdError; @code = 'processing mode conflict'; end
      class InvalidFrame < JsonLdError; @code = 'invalid frame'; end
      class InvalidEmbedValue < InvalidFrame; @code = 'invalid @embed value'; end
    end
  end
end


text:
# frozen_string_literal: true

module JSON
  module LD
    ##
    # Streaming writer interface.
    #
    # Writes an array of statements serialized in expanded JSON-LD. No provision for turning rdf:first/rest into @list encodings.
    # @author [Gregg Kellogg](http://greggkellogg.net/)
    module StreamingWriter
      ##
      # Write out array start, and note not to prepend node-separating ','
      # @return [void] `self`
      def stream_prologue
        # If we were provided a context, or prefixes, use them to compact the output
        @context = case @options[:context]
        when nil then nil
        when Context then @options[:context]
        else Context.parse(@options[:context])
        end

        # log_debug("prologue") {"context: #{context.inspect}"}
        if context
          @output.puts %({"@context": #{context.serialize['@context'].to_json}, "@graph": [)
        else
          @output.puts "["
        end
        self
      end

      ##
      # Write a statement, creating a current node definition, if necessary.
      #
      # Once a new/first statement is seen, terminate the current node definition and compact if provided a context.
      #
      # Also expects all statements in the same context to be contained in a block including all subjects in a block (except for list elements)
      #
      # Note that if list elements are not received in order using the same subject and property, this may cause a bad serialization.
      #
      # @return [void] `self`
      def stream_statement(statement)
        # log_debug("ss") {"state: #{@state.inspect}, stmt: #{statement}"}
        if @current_graph != statement.graph_name
          end_graph
          start_graph(statement.graph_name)
        end

        # If we're writing a list
        @current_node_def ||= { '@id' => statement.subject.to_s }

        if statement.subject.to_s != @current_node_def['@id']
          end_node
          @current_node_def = { '@id' => statement.subject.to_s }
        end

        if statement.predicate == RDF.type
          (@current_node_def['@type'] ||= []) << statement.object.to_s
        else
          pd = (@current_node_def[statement.predicate.to_s] ||= [])

          pd << if statement.object.resource?
            { '@id' => statement.object.to_s }
          elsif statement.object.datatype == RDF_JSON
            { "@value" => MultiJson.load(statement.object.to_s), "@type" => "@json" }
          else
            lit = { "@value" => statement.object.to_s }
            lit["@type"] = statement.object.datatype.to_s if statement.object.datatype?
            lit["@language"] = statement.object.language.to_s if statement.object.language?
            lit
          end
        end
        self
      end

      ##
      # Complete open statements
      # @return [void] `self`
      def stream_epilogue
        # log_debug("epilogue") {"state: #{@state.inspect}"}
        end_graph
        if context
          @output.puts "\n]}"
        else
          @output.puts "\n]"
        end
        self
      end

      private

      def start_graph(resource)
        # log_debug("start_graph") {"state: #{@state.inspect}, resource: #{resource}"}
        if resource
          @output.puts(",") if %i[wrote_node wrote_graph].include?(@state)
          @output.puts %({"@id": "#{resource}", "@graph": [)
          @state = :in_graph
        end
        @current_graph = resource
      end

      def end_graph
        # log_debug("end_graph") {"state: #{@state.inspect}, ctx: #{@current_graph}"}
        end_node
        return unless @current_graph

        @output.write %(]})
        @state = :wrote_graph
      end

      def end_node
        # log_debug("end_node") {"state: #{@state.inspect}, node: #{@current_node_def.to_json}"}
        @output.puts(",") if %i[wrote_node wrote_graph].include?(@state)
        return unless @current_node_def

        node_def = if context
          compacted = JSON::LD::API.compact(@current_node_def, context, rename_bnodes: false, **@options)
          compacted.delete('@context')
          compacted
        else
          @current_node_def
        end
        @output.write node_def.to_json
        @state = :wrote_node
        @current_node_def = nil
      end
    end
  end
end