Skip to content

Commit

Permalink
WIP support for LaTeX citations in text editor
Browse files Browse the repository at this point in the history
  • Loading branch information
krassowski committed Jun 19, 2022
1 parent 70662b8 commit b521a5a
Show file tree
Hide file tree
Showing 12 changed files with 1,728 additions and 1,403 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module.exports = {
{ avoidEscape: true, allowTemplateLiterals: false }
],
curly: ['error', 'all'],
eqeqeq: 'error',
eqeqeq: ['error', 'smart'],
'prefer-arrow-callback': 'error'
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"@jupyterlab/builder": "^3.0.0",
"@jupyterlab/cells": "^3.0.0",
"@jupyterlab/docregistry": "^3.0.11",
"@jupyterlab/fileeditor": "^3.0.11",
"@jupyterlab/notebook": "^3.0.11",
"@jupyterlab/statedb": "^3.0.6",
"@jupyterlab/statusbar": "^3.0.9",
Expand Down
15 changes: 15 additions & 0 deletions schema/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@
"command": "cm:update-references",
"keys": ["Alt U"],
"selector": ".jp-Notebook"
},
{
"command": "cm:insert-citation",
"keys": ["Alt C"],
"selector": ".jp-FileEditor"
},
{
"command": "cm:insert-bibliography",
"keys": ["Alt B"],
"selector": ".jp-FileEditor"
},
{
"command": "cm:update-references",
"keys": ["Alt U"],
"selector": ".jp-FileEditor"
}
],
"jupyter.lab.setting-icon": "citation:bibliography",
Expand Down
162 changes: 162 additions & 0 deletions src/adapters/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { IDocumentWidget } from '@jupyterlab/docregistry';
import { FileEditor } from '@jupyterlab/fileeditor';
import { CodeEditor } from '@jupyterlab/codeeditor';

import {
CitationInsertData,
CitationQuerySubset,
CiteProc,
ICitableItemRecordsBySource,
ICitation,
ICitationFormattingOptions,
IDocumentAdapter
} from '../types';
import OutputMode = CiteProc.OutputMode;
import { HTMLFormatter, TexFormatter, IOutputFormatter } from '../formatting';


function editorContentSubset(editor: CodeEditor.IEditor, subset: CitationQuerySubset): string {
const cursor = editor.getCursorPosition();
const offset = editor.getOffsetAt(cursor);
const content = editor.model.value.text;
switch (subset) {
case 'all':
return content;
case 'after-cursor':
return content.substring(offset);
case 'before-cursor':
return content.substring(0, offset);
}
}

export class EditorAdapter implements
IDocumentAdapter<IDocumentWidget<FileEditor>>
{
citations: ICitation[];

constructor(
public document: IDocumentWidget<FileEditor>,
public options: ICitationFormattingOptions
) {
this.citations = [];
}

getCitableItemsFallbackData(): ICitableItemRecordsBySource | null {
// currently not supported for editors
return null;
}

setCitableItemsFallbackData(data: ICitableItemRecordsBySource): void {
// currently not supported for editors
}

isAvailable(): boolean {
return this.outputFormat === 'latex';
}

private insertAtCursor(text: string) {
const fileEditor = this.document.content;
if (fileEditor) {
const editor = fileEditor.editor;
const cursor = editor.getCursorPosition();
const offset = editor.getOffsetAt(cursor);
fileEditor.model.value.insert(offset, text);
const updatedPosition = editor.getPositionAt(offset + text.length);
if (updatedPosition) {
editor.setCursorPosition(updatedPosition);
}
}
}

get outputFormat(): OutputMode {
// TODO: app.docRegistry.getFileTypeForModel(contentsModel)
const codeMirrorMimeType = this.document.content.model.mimeType;
if (codeMirrorMimeType === 'text/html') {
return 'html';
} else if (['text/x-latex', 'text/x-tex'].includes(codeMirrorMimeType)) {
return 'latex';
} else {
return this.options.defaultFormat;
}
}

getCitationStyle(): string | undefined {
// TODO
return;
}

setCitationStyle(newStyle: string): void {
// TODO - as metadata in frontmatter for Markdown?
return;
}

protected get formatter(): IOutputFormatter {
if (this.outputFormat === 'latex') {
return new TexFormatter(this.options);
}
return new HTMLFormatter(this.options);
}

insertBibliography(bibliography: string): void {
this.insertAtCursor(this.formatBibliography(bibliography));
}

formatBibliography(bibliography: string): string {
return this.formatter.formatBibliography(bibliography);
}

updateBibliography(bibliography: string): void {
const newText = this.formatter.updateBibliography(
this.document.content.model.value.text,
bibliography
);
if (newText != null) {
this.document.content.model.value.text = newText;
}
}

formatCitation(citation: CitationInsertData): string {
return this.formatter.formatCitation(citation);
}

insertCitation(citation: CitationInsertData): void {
this.insertAtCursor(this.formatCitation(citation));
// TODO: maybe store current citations in metadata (how?)
}

updateCitation(citation: ICitation): void {
const oldText = this.document.content.model.value.text;
const { newText, matchesCount } = this.formatter.updateCitation(
oldText,
citation
)
if (newText != null) {
this.document.content.model.value.text = newText;
}
if (matchesCount === 0) {
console.warn('Failed to update citation', citation, '- no matches found');
} else if (matchesCount > 1) {
console.warn(
'Citation',
citation,
'appears in more than once with the same ID; please correct it manually'
);
}
}

findCitations(subset: CitationQuerySubset): ICitation[] {

const fileEditor = this.document.content;
if (!fileEditor) {
throw Error('Editor not available')
}
const editor = fileEditor.editor;
return this.formatter.extractCitations(
editorContentSubset(editor, subset),
{
host: this.document.content.node
},
{}
);
}
}
112 changes: 32 additions & 80 deletions src/adapters/notebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ import type { INotebookModel, NotebookPanel } from '@jupyterlab/notebook';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import { DisposableDelegate, IDisposable } from '@lumino/disposable';
import { CommandToolbarButton } from '@jupyterlab/apputils';
import { extractCitations } from '../utils';
import { JupyterFrontEnd } from '@jupyterlab/application';
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
import ICellMetadata = NotebookAdapter.ICellMetadata;
import type { Cell } from '@jupyterlab/cells';
import OutputMode = CiteProc.OutputMode;
import { itemIdToPrimitive } from '../index';
import { HTMLFormatter, HybridFormatter, IOutputFormatter } from '../formatting';

export namespace NotebookAdapter {
export interface INotebookMetadata extends ReadonlyPartialJSONObject {
Expand Down Expand Up @@ -50,6 +49,7 @@ export namespace NotebookAdapter {
export const notebookMetadataKey = 'citation-manager';
export const cellMetadataKey = 'citation-manager';


export class NotebookAdapter implements IDocumentAdapter<NotebookPanel> {
citations: ICitation[];

Expand All @@ -71,12 +71,16 @@ export class NotebookAdapter implements IDocumentAdapter<NotebookPanel> {
this.updateCellMetadata();
}

isAvailable(): boolean {
return true;
}

private insertAtCursor(text: string) {
const activeCell = this.document.content.activeCell;
if (activeCell) {
const cursor = activeCell.editor.getCursorPosition();
const offset = activeCell.editor.getOffsetAt(cursor);
const editor = activeCell.editor;
const cursor = editor.getCursorPosition();
const offset = editor.getOffsetAt(cursor);
activeCell.model.value.insert(offset, text);
const updatedPosition = editor.getPositionAt(offset + text.length);
if (updatedPosition) {
Expand Down Expand Up @@ -155,37 +159,23 @@ export class NotebookAdapter implements IDocumentAdapter<NotebookPanel> {
});
}

protected get formatter(): IOutputFormatter {
if (this.outputFormat === 'latex') {
return new HybridFormatter(this.options);
}
return new HTMLFormatter(this.options);
}

insertBibliography(bibliography: string): void {
this.insertAtCursor(this.formatBibliography(bibliography));
}

formatBibliography(bibliography: string): string {
console.log('format', this.outputFormat);
if (this.outputFormat === 'latex') {
return bibliography;
}
return `<!-- BIBLIOGRAPHY START -->${bibliography}<!-- BIBLIOGRAPHY END -->`;
return this.formatter.formatBibliography(bibliography)
}

formatCitation(citation: CitationInsertData): string {
// note: not using `wrapCitationEntry` as that was causing more problems
// (itemID undefined).
let text = citation.text;
if (this.outputFormat === 'html' && this.options.linkToBibliography) {
// link to the first mentioned element
const first = citation.items[0];
const firstID = itemIdToPrimitive(first);
// encode the link as pipe symbol was causing issues with markdown tables,
// see https://github.com/krassowski/jupyterlab-citation-manager/issues/50
const encodedfirstID = encodeURIComponent(firstID);
text = `<a href="#${encodedfirstID}">${text}</a>`;
} else if (this.outputFormat === 'latex') {
// this does not work well with MathJax - we need to figure out something else!
// but it might still be useful (without $) for text editor adapter
// const citationIDs = citation.items.map(itemIdToPrimitive).join(',');
// text = `$$\\cite{${citationIDs}}$$`;
}
return `<cite id="${citation.citationId}">${text}</cite>`;
return this.formatter.formatCitation(citation)
}

insertCitation(citation: CitationInsertData): void {
Expand All @@ -194,6 +184,7 @@ export class NotebookAdapter implements IDocumentAdapter<NotebookPanel> {
if (!activeCell) {
return;
}
// TODO: maybe store current citations in metadata (how?)
const old =
(activeCell.model.metadata.get(cellMetadataKey) as ICellMetadata) || {};
activeCell.model.metadata.set(cellMetadataKey, {
Expand All @@ -205,23 +196,16 @@ export class NotebookAdapter implements IDocumentAdapter<NotebookPanel> {
}

updateCitation(citation: ICitation): void {
const pattern = new RegExp(
`<cite id=["']${citation.citationId}["'][^>]*?>([\\s\\S]*?)<\\/cite>`
);
let matches = 0;
this.nonCodeCells.forEach(cell => {
const oldText = cell.model.value.text;
const matchIndex = oldText.search(pattern);
if (matchIndex !== -1) {
const newCitation = this.formatCitation(citation);
const old = oldText.slice(matchIndex, matchIndex + newCitation.length);
if (newCitation !== old) {
cell.model.value.text = oldText.replace(
pattern,
this.formatCitation(citation)
);
}
matches += 1;
const { newText, matchesCount } = this.formatter.updateCitation(
oldText,
citation
)
matches += matchesCount;
if (newText != null) {
cell.model.value.text = newText;
}
});
if (matches === 0) {
Expand All @@ -236,45 +220,13 @@ export class NotebookAdapter implements IDocumentAdapter<NotebookPanel> {
}

updateBibliography(bibliography: string): void {
const htmlPattern =
/(?<=<!-- BIBLIOGRAPHY START -->)([\s\S]*?)(?=<!-- BIBLIOGRAPHY END -->)/;
const htmlFullyCapturingPattern =
/(<!-- BIBLIOGRAPHY START -->[\s\S]*?<!-- BIBLIOGRAPHY END -->)/;
const latexPattern =
/(\\begin{thebibliography}[\s\S]*?\\end{thebibliography})/;

this.nonCodeCells.forEach(cell => {
const oldText = cell.model.value.text;
if (oldText.match(/<!-- BIBLIOGRAPHY START -->/)) {
cell.model.value.text = oldText.replace(
this.outputFormat === 'latex'
? htmlFullyCapturingPattern
: htmlPattern,
this.outputFormat === 'latex' ? bibliography.trim() : bibliography
);
if (oldText.search(htmlPattern) === -1) {
console.warn(
'Failed to update bibliography',
bibliography,
'in',
oldText
);
}
} else if (oldText.match(/\\begin{thebibliography}/)) {
cell.model.value.text = oldText.replace(
latexPattern,
this.outputFormat !== 'latex'
? this.formatBibliography(bibliography)
: bibliography.trim()
);
if (oldText.search(latexPattern) === -1) {
console.warn(
'Failed to update bibliography',
bibliography,
'in',
oldText
);
}
const newText = this.formatter.updateBibliography(
cell.model.value.text,
bibliography
);
if (newText != null) {
cell.model.value.text = newText;
}
});
}
Expand Down Expand Up @@ -305,7 +257,7 @@ export class NotebookAdapter implements IDocumentAdapter<NotebookPanel> {
const cellMetadata = cell.model.metadata.get(cellMetadataKey) as
| NotebookAdapter.ICellMetadata
| undefined;
const cellCitations = extractCitations(
const cellCitations = this.formatter.extractCitations(
cell.model.value.text,
{
host: cell.node
Expand Down
Loading

0 comments on commit b521a5a

Please sign in to comment.