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

feat: extension point for myst options #115

Open
wants to merge 9 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
13 changes: 12 additions & 1 deletion src/MySTContentFactory.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { MarkdownCell } from '@jupyterlab/cells';
import { NotebookPanel, StaticNotebook } from '@jupyterlab/notebook';
import { MySTMarkdownCell } from './MySTMarkdownCell';
import { MySTNotebookOptions, MySTNotebookDefaults } from './myst';

export class MySTContentFactory extends NotebookPanel.ContentFactory {
mystOptions: MySTNotebookOptions;

constructor(
options = {},
mystOptions = new MySTNotebookDefaults() as MySTNotebookOptions
) {
super(options);
this.mystOptions = mystOptions;
}

createMarkdownCell(
options: MarkdownCell.IOptions,
parent: StaticNotebook
): MarkdownCell {
if (!options.contentFactory) {
options.contentFactory = this;
}
return new MySTMarkdownCell(options).initializeState();
return new MySTMarkdownCell(options, this.mystOptions).initializeState();
}
}
25 changes: 22 additions & 3 deletions src/MySTMarkdownCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '@myst-theme/providers';
import { render } from 'react-dom';
import { useParse } from 'myst-to-react';
import { parseContent } from './myst';
import { parseContent, MySTNotebookOptions } from './myst';
import { IMySTMarkdownCell } from './types';
import { linkFactory } from './links';
import { selectAll } from 'unist-util-select';
Expand All @@ -29,22 +29,38 @@ export class MySTMarkdownCell
implements IMySTMarkdownCell
{
private _doneRendering = new PromiseDelegate<void>();
private _doRendering = false;

mystOptions: MySTNotebookOptions;

myst: {
pre?: GenericParent;
post?: GenericParent;
node?: HTMLDivElement;
} = {};

constructor(options: MarkdownCell.IOptions) {
constructor(
options: MarkdownCell.IOptions,
mystOptions: MySTNotebookOptions
) {
super(options);
this.mystOptions = mystOptions;

// BUG: if renderInput() was attempted from super constructor, now it can be done
if (this._doRendering) this.renderInput(null as unknown as Widget);
this._doRendering = true;
Comment on lines +50 to +51
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this because downstream you are calling this constructor?

Do we need this render in here at all? Or can we remove it + the option and explicitly call renderInput when we need to?

Copy link
Contributor Author

@tavin tavin Mar 21, 2023

Choose a reason for hiding this comment

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

It's all just a horrible hack because of a bug in the 3.5.x parent class. A cursory look tells me it's fixed in the 3.6.x series. I don't know when it's okay for jupyterlab-myst to start following v3.6. But once the bug is gone, this code can be gone.


// Listen for changes to the cell trust
const trusted = this.model.modelDB.get('trusted') as ObservableValue;
trusted.changed.connect(this.mystRender, this);
}

renderInput(_: Widget): void {
if (!this._doRendering) {
// BUG: super constructor calls renderInput() but (of course) object isn't fully constructed
this._doRendering = true;
return;
}
if (!this.myst || !this.myst.node) {
// Create the node if it does not exist
const node = document.createElement('div');
Expand All @@ -54,7 +70,10 @@ export class MySTMarkdownCell
this._doneRendering = new PromiseDelegate<void>();
const notebook = this.parent as StaticNotebook;
this.myst.pre = undefined;
const parseComplete = parseContent(notebook);
const parseComplete = parseContent(
notebook,
this.mystOptions.get(notebook)
);
const widget = new Widget({ node: this.myst.node });
widget.addClass('myst');
widget.addClass('jp-MarkdownOutput');
Expand Down
29 changes: 27 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
JupyterFrontEndPlugin
} from '@jupyterlab/application';

import { Token } from '@lumino/coreutils';
import { IEditorServices } from '@jupyterlab/codeeditor';

import {
Expand All @@ -14,6 +15,7 @@ import {
Notebook
} from '@jupyterlab/notebook';
import { Cell } from '@jupyterlab/cells';
import { MySTNotebookOptions } from './myst';
import { MySTContentFactory } from './MySTContentFactory';

import { ISessionContextDialogs } from '@jupyterlab/apputils';
Expand All @@ -30,18 +32,41 @@ const mystIcon = new LabIcon({
svgstr: mystIconSvg
});

/**
* Extension point for MyST options to be defined given a notebook.
* A null provider results in default parser options appropriate to all notebooks.
*
* Notes for labextension authors:
*
* - This defines a mechanism for controlling myst *parser* options.
* - To set options globally just ignore the notebook in `get(notebook)`.
* - Various myst extensions to common markdown can be toggled on/off.
* - Math options can be tuned.
* - Custom directives and roles can be injected.
* - It's *not* a general system for extending myst syntax.
* - IT'S EXPERIMENTAL AND MIGHT DISAPPEAR IN A FUTURE RELEASE!
*/
export const IMySTNotebookOptions = new Token<MySTNotebookOptions>(
'jupyterlab-myst:IMySTNotebookOptions'
);

/**
* The notebook content factory provider.
*/
const plugin: JupyterFrontEndPlugin<NotebookPanel.IContentFactory> = {
id: 'jupyterlab-myst:plugin',
provides: NotebookPanel.IContentFactory,
requires: [IEditorServices],
optional: [IMySTNotebookOptions],
autoStart: true,
activate: (app: JupyterFrontEnd, editorServices: IEditorServices) => {
activate: (
app: JupyterFrontEnd,
editorServices: IEditorServices,
mystOptions: MySTNotebookOptions
) => {
console.log('JupyterLab extension jupyterlab-myst is activated!');
const editorFactory = editorServices.factoryService.newInlineEditor;
return new MySTContentFactory({ editorFactory });
return new MySTContentFactory({ editorFactory }, mystOptions || undefined);
}
};

Expand Down
56 changes: 43 additions & 13 deletions src/myst.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mystParse } from 'myst-parser';
import { mystParse, AllOptions } from 'myst-parser';
import { liftChildren } from 'myst-common';
import {
mathPlugin,
Expand Down Expand Up @@ -37,6 +37,38 @@ import { imageUrlSourceTransform } from './images';
import { internalLinksPlugin } from './links';
import { addCiteChildrenPlugin } from './citations';

export type MySTOptions = {
parserOptions: Partial<AllOptions>;
};

export type MySTOptionsProvider<Widget> = {
get(widget: Widget): MySTOptions;
};

/**
* The interface which must be implemented to customize options for notebooks.
*/
export type MySTNotebookOptions = MySTOptionsProvider<StaticNotebook>;

/**
* Global default myst options for notebooks.
*/
export class MySTNotebookDefaults implements MySTNotebookOptions {
get(notebook: StaticNotebook): MySTOptions {
return {
parserOptions: {
directives: [
cardDirective,
gridDirective,
proofDirective,
...tabDirectives
],
roles: [evalRole]
}
};
}
}

const evalRole: RoleSpec = {
name: 'eval',
body: {
Expand All @@ -49,16 +81,11 @@ const evalRole: RoleSpec = {
}
};

export function markdownParse(text: string): Root {
const mdast = mystParse(text, {
directives: [
cardDirective,
gridDirective,
proofDirective,
...tabDirectives
],
roles: [evalRole]
});
export function markdownParse(
text: string,
options: Partial<AllOptions>
): Root {
const mdast = mystParse(text, options);
// Parsing individually here requires that link and footnote references are contained to the cell
// This is consistent with the current Jupyter markdown renderer
unified()
Expand All @@ -79,7 +106,10 @@ export function markdownParse(text: string): Root {
return mdast as Root;
}

export function parseContent(notebook: StaticNotebook): Promise<void> {
export function parseContent(
notebook: StaticNotebook,
options: MySTOptions
): Promise<void> {
const cells = getCellList(notebook)?.filter(
// In the future, we may want to process the code cells as well, but not now
cell => cell.model.type === 'markdown'
Expand All @@ -93,7 +123,7 @@ export function parseContent(notebook: StaticNotebook): Promise<void> {
const text = cell.model?.value.text ?? '';
if (!cell.myst.pre) {
// This will be cleared when the cell is executed, and parsed again here
cell.myst.pre = markdownParse(text);
cell.myst.pre = markdownParse(text, options.parserOptions);
}
return { type: 'block', children: copyNode(cell.myst.pre).children };
});
Expand Down