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

WIP support for LaTeX citations in text editor #58

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
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