-
Notifications
You must be signed in to change notification settings - Fork 19
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
} | ||
} | ||
}); | ||
} | ||
} |
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[]) { | ||
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); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
@@ -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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]; |
There was a problem hiding this comment.
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!