diff --git a/src/bibliography.ts b/src/bibliography.ts index ce57680..2d243de 100644 --- a/src/bibliography.ts +++ b/src/bibliography.ts @@ -1,6 +1,6 @@ import { Token } from '@lumino/coreutils'; import { getCitations, CitationRenderer } from 'citation-js-utils'; -import { Contents, ContentsManager } from '@jupyterlab/services'; +import { Contents } from '@jupyterlab/services'; import { ISignal, Signal } from '@lumino/signaling'; export interface IBibliographyManager { diff --git a/src/citations.tsx b/src/citations.tsx index 0728d13..9ccad52 100644 --- a/src/citations.tsx +++ b/src/citations.tsx @@ -1,20 +1,101 @@ +import type { CitationRenderer } from 'citation-js-utils'; +import { InlineCite } from 'citation-js-utils'; import type { Plugin } from 'unified'; -import type { Root } from 'myst-spec'; -import type { GenericNode } from 'myst-common'; +import type { StaticPhrasingContent, Parent, Root } from 'myst-spec'; +import type { References } from 'myst-common'; import { selectAll } from 'unist-util-select'; +import type { Cite, CiteKind, CiteGroup } from 'myst-spec-ext'; -/** - * Add fake children to the citations - */ -export async function addCiteChildrenTransform(tree: Root): Promise { - const links = selectAll('cite', tree) as GenericNode[]; - links.forEach(async cite => { - if (cite.children && cite.children.length > 0) return; - cite.error = true; - cite.children = [{ type: 'text', value: cite.label }]; +function pushCite( + references: Pick, + citeRenderer: CitationRenderer, + label: string +) { + if (!references.cite) { + references.cite = { order: [], data: {} }; + } + if (!references.cite?.data[label]) { + references.cite.order.push(label); + } + references.cite.data[label] = { + // TODO: this number isn't right? Should be the last time it was seen, not the current size. + number: references.cite.order.length, + doi: citeRenderer[label]?.getDOI(), + html: citeRenderer[label]?.render() + }; +} + +export function combineCitationRenderers(renderers: CitationRenderer[]) { + const combined: CitationRenderer = {}; + renderers.forEach(renderer => { + Object.keys(renderer).forEach(key => { + if (combined[key]) { + console.log(`Duplicate citation with id: ${key}`); + } + combined[key] = renderer[key]; + }); }); + return combined; +} + +function addCitationChildren( + cite: Cite, + renderer: CitationRenderer, + kind: CiteKind = 'parenthetical' +): boolean { + const render = renderer[cite.label as string]; + try { + const children = render?.inline( + kind === 'narrative' ? InlineCite.t : InlineCite.p, + { + prefix: cite.prefix, + suffix: cite.suffix + } + ) as StaticPhrasingContent[]; + if (children) { + cite.children = children; + return true; + } + } catch (error) { + // pass + } + cite.error = true; + return false; +} + +function hasChildren(node: Parent) { + return node.children && node.children.length > 0; } -export const addCiteChildrenPlugin: Plugin<[], Root, Root> = () => tree => { - addCiteChildrenTransform(tree); +type Options = { + renderer: CitationRenderer; + references: Pick; }; + +export function transformCitations(mdast: Root, opts: Options) { + // TODO: this can be simplified if typescript doesn't die on the parent + const citeGroups = selectAll('citeGroup', mdast) as CiteGroup[]; + citeGroups.forEach(node => { + const kind = node.kind; + node.children?.forEach(cite => { + addCitationChildren(cite, opts.renderer, kind); + }); + }); + const citations = selectAll('cite', mdast) as Cite[]; + citations.forEach(cite => { + const citeLabel = cite.label as string; + // push cites in order of appearance in the document + pushCite(opts.references, opts.renderer, citeLabel); + if (hasChildren(cite)) return; + // These are picked up as they are *not* cite groups + const success = addCitationChildren(cite, opts.renderer); + if (!success) { + console.error(`⚠️ Could not find citation: ${cite.label}`); + } + }); +} + +export const addCiteChildrenPlugin: Plugin<[Options], Root, Root> = + opts => (tree, vfile) => { + transformCitations(tree, opts); + }; diff --git a/src/index.ts b/src/index.ts index 341719e..035e94d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { notebookCellExecuted } from './actions'; import { mystMarkdownRendererFactory } from './mime'; +import { citationRenderers } from './myst'; /** * The notebook content factory provider. @@ -83,12 +84,20 @@ const bibPlugin: JupyterFrontEndPlugin = { activate: (app: JupyterFrontEnd) => { console.log('Using jupyterlab-myst:bibliography'); + const bibFile = 'bibliography.bib'; const manager = new BibliographyManager( app.serviceManager.contents, - 'bibliography.bib' + bibFile ); manager.changed.connect((manager, renderer) => { console.log(renderer, 'CHANGE'); + // TODO: not sure how to pass this state over to the myst renderer. We need some global state? + // If that is the case, we can do that using redux. + if (renderer) { + citationRenderers[bibFile] = renderer; + } else { + delete citationRenderers[bibFile]; + } }); return manager; } diff --git a/src/myst.ts b/src/myst.ts index e0932dc..ce770bf 100644 --- a/src/myst.ts +++ b/src/myst.ts @@ -32,13 +32,16 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; import { imageUrlSourceTransform } from './images'; import { internalLinksTransform } from './links'; -import { addCiteChildrenPlugin } from './citations'; +import { addCiteChildrenPlugin, combineCitationRenderers } from './citations'; +import { CitationRenderer } from 'citation-js-utils'; import { evalRole } from './roles'; import { IUserExpressionMetadata } from './metadata'; import { IMySTMarkdownCell } from './types'; import { Cell, ICellModel } from '@jupyterlab/cells'; import { MySTModel } from './widget'; +export const citationRenderers: Record = {}; + export interface IMySTDocumentState { references: References; frontmatter: PageFrontmatter; @@ -111,6 +114,11 @@ export async function processArticleMDAST( numbering: frontmatter.numbering, file }); + + const renderer = combineCitationRenderers( + Object.entries(citationRenderers).map(([, v]) => v) + ); + unified() .use(mathPlugin, { macros: frontmatter?.math ?? {} }) // This must happen before enumeration, as it can add labels .use(glossaryPlugin, { state }) // This should be before the enumerate plugins @@ -119,7 +127,7 @@ export async function processArticleMDAST( .use(linksPlugin, { transformers: linkTransforms }) .use(footnotesPlugin) .use(resolveReferencesPlugin, { state }) - .use(addCiteChildrenPlugin) + .use(addCiteChildrenPlugin, { references, renderer }) .use(keysPlugin) .runSync(mdast as any, file); @@ -177,6 +185,10 @@ export async function processNotebookMDAST( file }); + const renderer = combineCitationRenderers( + Object.entries(citationRenderers).map(([, v]) => v) + ); + unified() .use(mathPlugin, { macros: frontmatter?.math ?? {} }) // This must happen before enumeration, as it can add labels .use(glossaryPlugin, { state }) // This should be before the enumerate plugins @@ -185,7 +197,7 @@ export async function processNotebookMDAST( .use(linksPlugin, { transformers: linkTransforms }) .use(footnotesPlugin) .use(resolveReferencesPlugin, { state }) - .use(addCiteChildrenPlugin) + .use(addCiteChildrenPlugin, { references, renderer }) .use(keysPlugin) .runSync(mdast as any, file);