diff --git a/package.json b/package.json index 5b2aed2545..20885d26f8 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "build": "npm run build:jsonschema && npm run build:jsonld && npm run build:ts && npm run build:py && npm run build:r", "build:jsonschema": "ts-node ts/schema.ts", "build:jsonld": "ts-node ts/bindings/jsonld.ts", - "build:ts": "ts-node ts/bindings/typescript.ts && tsc && cp built/*.schema.json dist", + "build:ts": "ts-node ts/bindings/typescript.ts && tsc && cp built/*.schema.json dist && cp built/*.jsonld dist", "build:py": "ts-node ts/bindings/python.ts", "build:r": "ts-node ts/bindings/r.ts", "docs": "npm run docs:readme && npm run docs:build", diff --git a/ts/bindings/jsonld.ts b/ts/bindings/jsonld.ts index 8c00d37895..aba46192d1 100644 --- a/ts/bindings/jsonld.ts +++ b/ts/bindings/jsonld.ts @@ -2,17 +2,21 @@ * Generate `built/*.jsonld` files from `schema/*.schema.yaml` files. * * For custom types (those not defined elsewhere) generates a JSON-LD - * file similar to e.g. https://schema.org/Person.jsonld + * file similar to those on schema.org e.g. https://schema.org/Person.jsonld * * For custom properties generates a JSON-LD file similar to - * e.g. https://schema.org/sibling.jsonld + * those on schema.org e.g. https://schema.org/sibling.jsonld */ import fs from 'fs-extra' import path from 'path' +// @ts-ignore +import fromEntries from 'object.fromentries' import { read } from './utils' export const build = async (): Promise => { + await fs.ensureDir(path.join(__dirname, '..', '..', 'built')) + const types: { [key: string]: {} } = {} const properties: { [key: string]: { '@id': string } & { [key: string]: unknown } @@ -20,29 +24,31 @@ export const build = async (): Promise => { const schemas = await read() for (const schema of schemas.values()) { - const { '@id': typeId, title, properties: typeProperties } = schema + const { + '@id': typeId, + title, + properties: typeProperties + } = schema - // Skip union types like `Node` and `BlockContent` that do not need to - // be represented here. + // Skip union types, like `Node` and `BlockContent`, that do not need to + // be represented in the `@context`. if (typeId === undefined || title === undefined || properties === undefined) continue - // Create a schema.org [`Class`](https://meta.schema.org/Class) for those - // types that are defined by this schema, otherwise link to the class defined - // in the other context. + // Create a schema.org [`Class`](https://meta.schema.org/Class) for + // types defined by this schema. if (typeId.startsWith('stencila:')) { - types[title] = { + const classs = { '@id': typeId, '@type': 'schema:Class', 'schema:name': title, 'schema:description': schema.description } - } else { - types[title] = { - '@id': typeId - } + } + types[typeId] = { '@id': typeId, name: title } + // Create a [`Property`](https://meta.schema.org/Property) for those // properties that are defined by this schema, otherwise link to the property // defined in the other context. @@ -51,12 +57,15 @@ export const build = async (): Promise => { // for an approach to that. if (typeProperties !== undefined) { for (const [name, property] of Object.entries(typeProperties)) { - const pid = property['@id'] - if (pid === undefined) continue + let pid = property['@id'] + // Do not add terms that are aliases with JSON-LD keywords: @id, @type etc + if (pid === undefined || name == 'id' || name === 'type' || name === 'value') continue + // The `schema` property clashes with the schema.org alias. So rename it... + if (pid === 'stencila:schema') pid = 'stencila:scheme' if (pid.startsWith('stencila:')) { if (properties[name] === undefined) { - properties[name] = { + properties[pid] = { '@id': pid, '@type': 'schema:Property', 'schema:name': name, @@ -70,46 +79,54 @@ export const build = async (): Promise => { } } } else { - properties[name] = { - '@id': pid + properties[pid] = { + '@id': pid, + name } } } } } - const jsonld = { - '@context': { - // Contexts referred to, including this one - schema: 'https://schema.org/', - bioschemas: 'http://bioschemas.org', - codemeta: 'https://doi.org/10.5063/schema/codemeta-2.0', - stencila: 'https://stencila.github.io/schema/01-draft', + /** + * The main JSON-LD @context. + * + * Written to be similar to schema.org's @context: + * https://schema.org/docs/jsonldcontext.jsonld + */ + const context = { + // Alias JSON-LD keywords e.g. `@type` and `@id` + // For why this is useful, see "Addressing the “@” issue" at + // https://datalanguage.com/news/publishing-json-ld-for-developers + type: '@type', + id: '@id', + value: '@value', - // Alias `@type` and `@id` - // See "Addressing the “@” issue" at https://datalanguage.com/news/publishing-json-ld-for-developers - // for why this is useful. - type: '@type', - id: '@id' - } - } + // Other contexts referred to, including this one + // Note that http vs https is important! + schema: 'http://schema.org/', + bioschemas: 'http://bioschemas.org', + codemeta: 'http://doi.org/10.5063/schema/codemeta-2.0', + stencila: 'http://schema.stenci.la/', - // Add types and properties alphabetically - for (const [key, value] of [ - ...[...Object.entries(types)].sort(), - ...[...Object.entries(properties)].sort() - ]) { - // @ts-ignore - jsonld[key] = value - } + // Define that in this context all terms derive from this vocabulary + // (and so do not need prefixing) + "@vocab": "http://schema.stenci.la/", - await fs.ensureDir(path.join(__dirname, '..', '..', 'built')) + // Types and properties added in alphabetical order after this e.g + // "schema:AudioObject": {"@id": "schema:AudioObject"}, + ...fromEntries([ + ...[...Object.keys(types)].sort(), + ...[...Object.keys(properties)].sort() + ].map((id: string) => { + const term = id.split(':')[1] + return [term, { '@id': id }] + })) + } await fs.writeJSON( path.join(__dirname, '..', '..', 'built', 'stencila.jsonld'), - jsonld, - { - spaces: 2 - } + { '@context': context }, + { spaces: 2 } ) }