Skip to content

Media types

A document’s media type defines how a session parses the document. It also defines what references and fragments mean in that document. A Hyperjump reads JRef and JSON by default:

Media type Content-Type Extension
JRef application/reference+json .jref
JSON application/json .json

Where a document’s media type comes from depends on where the document comes from:

Where a document comes from Where its media type comes from
HTTP Its Content-Type header, or its extension for URLs you list
A file Its extension

A media type plugin defines the mediaType it parses and a parse function that turns a Response into a JRef-compatible abstract syntax tree (AST). You configure plugins with the mediaTypePlugins config when you create the Hyperjump. This example is a plugin for a YAML version of JRef. It parses the YAML and passes the result to the JRef parser:

TypeScript
import {
fromJref,
Hyperjump,
jrefSyntaxErrorMessage
} from "@hyperjump/browser";
import type {
JrefMediaTypeDocument,
MediaTypePlugin,
RetrievedResponse
} from "@hyperjump/browser";
import YAML from "yaml";
type YrefDocument = JrefMediaTypeDocument;
class YrefMediaTypePlugin implements MediaTypePlugin<YrefDocument> {
mediaType = "application/reference+yaml";
extensions = [".yref"];
async parse(response: RetrievedResponse): Promise<YrefDocument> {
const json = JSON.stringify(YAML.parse(await response.text()));
const result = fromJref(json, response.url);
if (result.status !== "success") {
throw Error(jrefSyntaxErrorMessage(result.errors[0]));
}
return {
type: "jref-document",
status: result.status,
children: [result.node],
comments: [],
url: response.url
};
}
// A fragment is a JSON Pointer, as it is for JRef
fragmentPointer(_document: YrefDocument, fragment: string) {
return decodeURIComponent(fragment);
}
}
const h = new Hyperjump({
mediaTypePlugins: [new YrefMediaTypePlugin()]
});

A plugin has these properties:

Property Description
mediaType The media type it parses
extensions File extensions for it, such as .yref. A name that starts with / is a whole filename, such as /.bowerrc.
parse(response, session) Parses a RetrievedResponse into a document node. If parsing needs other documents, retrieve them with session. Throw if the document isn’t valid.
fragmentPointer(document, fragment) Returns the JSON Pointer a fragment identifies. Without it, the session ignores fragments.
quality Optional. Its quality in the Accept header, a number from 0 to 1 with at most three decimal places. The Hyperjump constructor throws for any other value.

A RetrievedResponse is a Response whose url the session parsed, without its fragment, so a plugin can use it as the document’s url and as the URL it resolves references against.

A plugin is a MediaTypePlugin<D>, where D is the type of the document node it parses. A Hyperjump can only browse documents made of JRef nodes. This plugin’s documents come from the JRef parser, so the plugin uses JrefMediaTypeDocument. That’s the built-in JRef plugin’s type for a document that parsed without errors.

Errors and tools use the positions in the AST to point at the source. This example loses YAML’s positions by going through JSON.parse, so a real plugin would build the AST from the YAML parser’s nodes instead.

A plugin for a media type that the Hyperjump already supports replaces the existing plugin and takes its place in the Accept header. When more than one plugin in the list has the same media type, the Hyperjump uses the later one.

Some media types let a document contain other documents, each with a URL of its own, the way a bundle does. A plugin for such a media type gives the document it parses an embedded property that maps each of those URLs to the document at it. Each embedded document gets the same embedded object:

TypeScript
const embedded: Record<string, YrefDocument> = {};
embedded["https://example.com/address"] = {
type: "jref-document",
status: result.status,
children: [addressNode],
comments: [],
url: "https://example.com/address",
embedded
};
return {
type: "jref-document",
status: result.status,
children: [result.node],
comments: [],
url: response.url,
embedded
};

A reference to https://example.com/address from the document that contains it finds the embedded document, so the session doesn’t retrieve it and allowRetrieval doesn’t apply. A reference from any other document doesn’t find it, so a document can’t change what another document’s references find by claiming a URL. An embedded document has the same embedded object, so a reference in it finds the other documents embedded with it. Its nodes’ locations can be in the document that contains it or start with its own URL, whichever your media type needs.

When you follow a reference yourself with getDocument, pass the document the reference is in as referencedIn so that the session looks in its embedded documents first.

Every request sends an Accept header that lists every media type the Hyperjump supports with its plugin’s quality. To ask servers for the one you prefer, lower the quality of the others. The JRef and JSON plugins take a quality option. This example replaces the default JRef plugin with one that gives JRef a quality of 0.9. JSON and YRef keep the default quality of 1, so servers get a request to prefer them:

TypeScript
import { Hyperjump, JrefMediaTypePlugin } from "@hyperjump/browser";
const h = new Hyperjump({
mediaTypePlugins: [
new JrefMediaTypePlugin({ quality: 0.9 }),
new YrefMediaTypePlugin()
]
});

With that, requests send this header:

Output
Accept: application/reference+json; q=0.9, application/json, application/reference+yaml, */*; q=0.001

A document from HTTP gets its media type from the Content-Type header that the server sends. Some servers send the wrong one or none at all. GitHub sends text/plain for every file at raw.githubusercontent.com, so a session doesn’t read those files as JSON. The file’s .json extension isn’t an authority on what the server sends, and anyone can give a URL a misleading extension.

A session uses extensions only for files and the URLs listed in the mediaTypeByExtension config. To trust a server’s extensions, list its URLs. In this example, documents from GitHub get their media type from their extension:

TypeScript
import { Hyperjump } from "@hyperjump/browser";
const h = new Hyperjump({
allowRetrieval: ["https://raw.githubusercontent.com/my-org/my-repo/"],
mediaTypeByExtension: ["https://raw.githubusercontent.com/"]
});

With that, a session parses a document as JSON when its URL is under https://raw.githubusercontent.com/ and ends with .json, whatever its Content-Type says. When no media type plugin defines the URL’s extension, the document keeps the media type the server sent. The extension is the end of the URL’s path, so a query such as the ?token= on a private repository’s raw URL doesn’t hide the extension.

URLs in mediaTypeByExtension match the way they do in allowRetrieval. A URL that ends with / matches every URL under it. Any other URL matches only itself. Every URL has to be absolute. When a server redirects a request, the session matches the URL that the redirect leads to because the server at that URL is the one that sent the document.