Skip to content

Commit

Permalink
馃 Add ROR hover links (#1198)
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanc1 committed May 10, 2024
1 parent 7c6f45c commit 1e24a9f
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .changeset/stupid-apples-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"myst-transforms": patch
"myst-common": patch
"myst-cli": patch
---

Add ROR link resolvers
11 changes: 11 additions & 0 deletions docs/external-references.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,14 @@ Click on one of the RRIDs below to see additional metadata!
- <rrid:SCR_014212>
```
````

## Research Organization Registry

The Research Organization Registry (ROR) is a global, community-led registry of open persistent identifiers for research organizations. You can add these to your MyST frontmatter or use the links directly in your documents.

To create an ROR link, use the `ror:` protocol followed by the identifier, for example:

- `[](ror:03rmrcq20)` becomes [](ror:03rmrcq20)
- `<ror:03rmrcq20>` becomes <ror:03rmrcq20>

You may also use a URL similar to `https://ror.org/03rmrcq20`. To find your organization use the search provided at [ror.org](https://ror.org)
4 changes: 4 additions & 0 deletions packages/myst-cli/src/process/mdast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
WikiTransformer,
GithubTransformer,
RRIDTransformer,
RORTransformer,
DOITransformer,
joinGatesPlugin,
glossaryPlugin,
Expand All @@ -48,6 +49,7 @@ import {
transformCitations,
transformImageFormats,
transformLinkedDOIs,
transformLinkedRORs,
transformOutputsToCache,
transformRenderInlineExpressions,
transformThumbnail,
Expand Down Expand Up @@ -198,12 +200,14 @@ export async function transformMdast(
new WikiTransformer(),
new GithubTransformer(),
new RRIDTransformer(),
new RORTransformer(),
new DOITransformer(), // This also is picked up in the next transform
new MystTransformer(Object.values(cache.$externalReferences)),
new SphinxTransformer(Object.values(cache.$externalReferences)),
];
linksTransform(mdast, vfile, { transformers, selector: LINKS_SELECTOR });
await transformMystXRefs(session, vfile, mdast, frontmatter);
await transformLinkedRORs(session, vfile, mdast, file);
// Initialize citation renderers for this (non-bib) file
cache.$citationRenderers[file] = await transformLinkedDOIs(
session,
Expand Down
1 change: 1 addition & 0 deletions packages/myst-cli/src/transforms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './citations.js';
export * from './code.js';
export * from './crossReferences.js';
export * from './dois.js';
export * from './ror.js';
export * from './embed.js';
export * from './images.js';
export * from './include.js';
Expand Down
133 changes: 133 additions & 0 deletions packages/myst-cli/src/transforms/ror.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import fs from 'node:fs';
import { join } from 'node:path';
import type { Link } from 'myst-spec';
import type { GenericNode, GenericParent } from 'myst-common';
import { RuleId, plural, fileError, toText } from 'myst-common';
import { selectAll } from 'unist-util-select';
import { computeHash, tic } from 'myst-cli-utils';
import type { VFile } from 'vfile';
import type { ISession } from '../session/types.js';

type RORResponse = {
id: string;
name: string;
};

/**
* Build a path to the cache-file for the given ROR
*
* @param session: CLI session
* @param ror: normalized ROR ID
*/
function rorFromCacheFile(session: ISession, ror: string) {
const filename = `ror-${computeHash(ror)}.json`;
const cacheFolder = join(session.buildPath(), 'cache');
if (!fs.existsSync(cacheFolder)) fs.mkdirSync(cacheFolder, { recursive: true });
return join(cacheFolder, filename);
}

/**
* Resolve the given ror.org ID into JSON data about the organization
*
* @param session - CLI session
* @param ror - ror.org ID
*/
export async function resolveRORAsJSON(
session: ISession,
ror: string,
): Promise<RORResponse | undefined> {
const url = `https://api.ror.org/organizations/${ror}`;
session.log.debug(`Fetching ROR JSON from ${url}`);
const response = await session.fetch(url).catch(() => {
session.log.debug(`Request to ${url} failed.`);
return undefined;
});
if (!response || !response.ok) {
session.log.debug(`ROR fetch failed for ${url}`);
return undefined;
}
const data = (await response.json()) as RORResponse;
return data;
}

/**
* Fetch organization data for the given ROR ID in JSON
*
* @param session - CLI session
* @param vfile
* @param node
* @param ror - ror ID (does not include the https://ror.org)
*/
export async function resolveROR(
session: ISession,
vfile: VFile,
node: GenericNode,
ror: string,
): Promise<RORResponse | undefined> {
if (!ror) return undefined;

// Cache ROR resolution as JSON
const cachePath = rorFromCacheFile(session, ror);

if (fs.existsSync(cachePath)) {
const cached = fs.readFileSync(cachePath).toString();
session.log.debug(`Loaded cached ROR response for https://ror.org/${ror}`);
return JSON.parse(cached);
}
const toc = tic();
let data;
try {
data = await resolveRORAsJSON(session, ror);
if (data) {
session.log.debug(toc(`Fetched ROR JSON for ror:${ror} in %s`));
} else {
fileError(vfile, `Could not find ROR "${ror}" from https://ror.org/${ror}`, {
node,
ruleId: RuleId.rorLinkValid,
note: `Please check the ROR is correct and has a page at https://ror.org/${ror}`,
});
session.log.debug(`JSON not available from ror.org for ror:${ror}`);
}
} catch (error) {
session.log.debug(`JSON from ror.org was malformed for ror:${ror}`);
}

if (!data) return undefined;
session.log.debug(`Saving ROR JSON to cache ${cachePath}`);
fs.writeFileSync(cachePath, JSON.stringify(data));
return data as unknown as RORResponse;
}

/**
* Find in-line RORs and add default text
*/
export async function transformLinkedRORs(
session: ISession,
vfile: VFile,
mdast: GenericParent,
path: string,
): Promise<void> {
const toc = tic();
const linkedRORs = selectAll('link[protocol=ror]', mdast) as Link[];
if (linkedRORs.length === 0) return;
session.log.debug(`Found ${plural('%s ROR(s)', linkedRORs.length)} to auto link.`);
let number = 0;
await Promise.all([
...linkedRORs.map(async (node) => {
const ror = node.data?.ror as string;
if (!ror) return;
number += 1;
const rorData = await resolveROR(session, vfile, node, ror);
console.log(rorData);
if (rorData && toText(node.children) === ror) {
// If the link text is the ROR, update with a organization name
node.children = [{ type: 'text', value: rorData.name }];
}
return true;
}),
]);
if (number > 0) {
session.log.info(toc(`馃獎 Linked ${plural('%s ROR(s)', number)} in %s for ${path}`));
}
return;
}
1 change: 1 addition & 0 deletions packages/myst-common/src/ruleids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export enum RuleId {
mystLinkValid = 'myst-link-valid',
sphinxLinkValid = 'sphinx-link-valid',
rridLinkValid = 'rrid-link-valid',
rorLinkValid = 'ror-link-valid',
wikipediaLinkValid = 'wikipedia-link-valid',
doiLinkValid = 'doi-link-valid',
linkResolves = 'link-resolves',
Expand Down
5 changes: 5 additions & 0 deletions packages/myst-transforms/docs/links.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
WikiTransformer,
GithubTransformer,
RRIDTransformer,
RORTransformer,
DOITransformer,
MystTransformer,
} from 'myst-transforms';
Expand All @@ -23,6 +24,7 @@ const transformers = [
new WikiTransformer(),
new GithubTransformer(),
new RRIDTransformer(),
new RORTransformer(),
new DOITransformer(),
new MystTransformer(intersphinx),
];
Expand All @@ -38,6 +40,9 @@ linksTransform(mdast, vfile, { transformers });
`RRIDTransformer`
: The RRID transformer picks up on RRIDs and allows these to be previewed.

`RORTransformer`
: The ROR transformer picks up on RORs and allows these to be previewed.

`DOITransformer`
: The DOI transformer can validate DOIs (using `doi-utils`) and provides interactive previews of citation information.

Expand Down
1 change: 1 addition & 0 deletions packages/myst-transforms/src/links/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { MystTransformer } from './myst.js';
export { SphinxTransformer } from './sphinx.js';
export { WikiTransformer } from './wiki.js';
export { RRIDTransformer } from './rrid.js';
export { RORTransformer } from './ror.js';
export { DOITransformer } from './doi.js';
export { GithubTransformer } from './github.js';
export type {
Expand Down
50 changes: 50 additions & 0 deletions packages/myst-transforms/src/links/ror.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { RuleId, fileWarn } from 'myst-common';
import type { VFile } from 'vfile';
import type { Link, LinkTransformer } from './types.js';
import { updateLinkTextIfEmpty, withoutHttp } from './utils.js';

const RESOLVER = 'https://ror.org/';
const TRANSFORM_SOURCE = 'LinkTransform:RORTransformer';

function isValid(rrid: string): boolean {
// TODO: regexp validation
return !!rrid;
}

function getROR(uri: string) {
if (uri.startsWith('ror:')) {
return uri.replace(/^ror:/, '').trim();
}
if (withoutHttp(uri).startsWith(withoutHttp(RESOLVER))) {
return withoutHttp(uri).replace(withoutHttp(RESOLVER), '').trim();
}
return uri.trim();
}

export class RORTransformer implements LinkTransformer {
protocol = 'ror';

test(uri?: string): boolean {
if (!uri) return false;
if (uri.startsWith('ror:')) return true;
return withoutHttp(uri).startsWith(withoutHttp(RESOLVER));
}

transform(link: Link, file: VFile): boolean {
const urlSource = link.urlSource || link.url;
const ror = getROR(urlSource);
if (!isValid(ror)) {
fileWarn(file, `ROR is not valid: ${urlSource}`, {
node: link,
source: TRANSFORM_SOURCE,
ruleId: RuleId.rorLinkValid,
});
return false;
}
link.url = `${RESOLVER}${ror}`;
link.data = { ...link.data, ror };
link.internal = false;
updateLinkTextIfEmpty(link, ror);
return true;
}
}

0 comments on commit 1e24a9f

Please sign in to comment.