Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸ§‘πŸ»β€πŸ”¬ Citations & bibliography #161

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"myst-parser": "^1.0.21",
"myst-to-html": "^1.0.21",
"myst-to-react": "^0.5.21",
"myst-transforms": "^1.1.19"
"myst-transforms": "^1.1.19",
"citation-js-utils": "^1.0.2"
},
"devDependencies": {
"@babel/core": "^7.0.0",
Expand Down
89 changes: 89 additions & 0 deletions src/bibliography.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Token } from '@lumino/coreutils';
import { getCitations, CitationRenderer } from 'citation-js-utils';
import { Contents } from '@jupyterlab/services';
import { ISignal, Signal } from '@lumino/signaling';

export interface IBibliographyManager {
getBibliography(): CitationRenderer | null;

changed: ISignal<this, CitationRenderer | null>;
}

export const IBibliographyManager = new Token<IBibliographyManager>(
'jupyterlab-myst:IBibliographyManager'
);

export class BibliographyManager implements IBibliographyManager {
private _renderer: CitationRenderer | null;
private _changed = new Signal<this, CitationRenderer | null>(this);

get changed(): ISignal<this, CitationRenderer | null> {
return this._changed;
}

getBibliography(): CitationRenderer | null {
return this._renderer;
}

constructor(contents: Contents.IManager, bibFile: string) {
this._renderer = null;

contents
.get(bibFile)
.then(async model => {
this._renderer = await getCitations(model.content);
this._changed.emit(this._renderer);
})
.catch();

// Handle changes
contents.fileChanged.connect(async (_, change) => {
// On create
if (change.type === 'new') {
const path = (change.newValue as Contents.IModel).path;
// Add model to record registry
if (path === bibFile) {
const model = await contents.get(path);
this._renderer = await getCitations(model.content);
this._changed.emit(this._renderer);
}
}
// On rename
else if (change.type === 'rename') {
// Remove by path
const oldPath = (change.oldValue as Contents.IModel).path;
// Add under new path!
const newPath = (change.newValue as Contents.IModel).path;
// Add model to record registry
if (newPath === bibFile) {
const model = await contents.get(newPath);
this._renderer = await getCitations(model.content);
this._changed.emit(this._renderer);
} else if (oldPath === bibFile) {
this._renderer = null;
this._changed.emit(this._renderer);
}
}
// On delete
else if (change.type === 'delete') {
const path = (change.oldValue as Contents.IModel).path;
// Add model to record registry
if (path === bibFile) {
this._renderer = null;
this._changed.emit(this._renderer);
}
}
// On save
else {
const path = (change.newValue as Contents.IModel).path;
// Add model to record registry
// Add model to record registry
if (path === bibFile) {
const model = await contents.get(path);
this._renderer = await getCitations(model.content);
this._changed.emit(this._renderer);
}
}
});
}
}
107 changes: 94 additions & 13 deletions src/citations.tsx
Original file line number Diff line number Diff line change
@@ -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<void> {
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<References, 'cite'>,
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[]) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bunch of this is copied from the CLI, we need to create a package, share this functionality and delete it here!

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<References, 'cite'>;
};

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);
};
32 changes: 31 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ import {
} from '@jupyterlab/notebook';
import { Cell } from '@jupyterlab/cells';
import { MySTContentFactory } from './MySTContentFactory';
import { IBibliographyManager, BibliographyManager } from './bibliography';

import { IRenderMimeRegistry } from '@jupyterlab/rendermime';

import { notebookCellExecuted } from './actions';
import { mystMarkdownRendererFactory } from './mime';
import { citationRenderers } from './myst';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agoose77 we need some way to share the citation renderer into the various places we use those renderer. I don't think they necessarily need to be "connected" (i.e. an update to a bib file doesn't really need to force a re-render, but the next render should bring in the right citation).

The way we have done this in the CLI is to have a state manager, that is indexed based on file name, and then we combine citation renderers before we render the markdown.

I think this could work, and maybe that works? Curious if you know of a better way to share state between the plugins?


/**
* The notebook content factory provider.
Expand Down Expand Up @@ -73,4 +76,31 @@ const mimeRendererPlugin: JupyterFrontEndPlugin<void> = {
}
};

export default [plugin, executorPlugin, mimeRendererPlugin];
const bibPlugin: JupyterFrontEndPlugin<IBibliographyManager> = {
id: 'jupyterlab-myst:bibliography',
requires: [],
provides: IBibliographyManager,
autoStart: true,
activate: (app: JupyterFrontEnd) => {
console.log('Using jupyterlab-myst:bibliography');

const bibFile = 'bibliography.bib';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now this only works for a single bib file in the root.

const manager = new BibliographyManager(
app.serviceManager.contents,
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];
}
Comment on lines +96 to +100
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the manager changes, we just update the shared state, which is then available next time we render markdown. I don't really think we need to go full: update-the bib when the markdown changes as it could potentially render every single cell.

});
return manager;
}
};

export default [plugin, executorPlugin, mimeRendererPlugin, bibPlugin];
18 changes: 15 additions & 3 deletions src/myst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, CitationRenderer> = {};

export interface IMySTDocumentState {
references: References;
frontmatter: PageFrontmatter;
Expand Down Expand Up @@ -108,6 +111,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
Expand All @@ -116,7 +124,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);

Expand Down Expand Up @@ -178,6 +186,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
Expand All @@ -186,7 +198,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);

Expand Down
Loading
Loading