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 2 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
12 changes: 11 additions & 1 deletion src/MySTContentFactory.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { MarkdownCell } from '@jupyterlab/cells';
import { NotebookPanel, StaticNotebook } from '@jupyterlab/notebook';
import { MySTMarkdownCell } from './MySTMarkdownCell';
import { MySTOptionsProvider, MySTNotebookDefaults } from './myst';

export class MySTContentFactory extends NotebookPanel.ContentFactory {

mystOptions: MySTOptionsProvider<StaticNotebook>;

constructor(options = {},
mystOptions = new MySTNotebookDefaults() as MySTOptionsProvider<StaticNotebook>) {
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();
}
}
19 changes: 16 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, MySTOptionsProvider } from './myst';
import { IMySTMarkdownCell } from './types';
import { linkFactory } from './links';
import { selectAll } from 'unist-util-select';
Expand All @@ -29,22 +29,35 @@ export class MySTMarkdownCell
implements IMySTMarkdownCell
{
private _doneRendering = new PromiseDelegate<void>();
private _doRendering = false;

mystOptions: MySTOptionsProvider<StaticNotebook>;

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

constructor(options: MarkdownCell.IOptions) {
constructor(options: MarkdownCell.IOptions, mystOptions: MySTOptionsProvider<StaticNotebook>) {
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
Member

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 +67,7 @@ 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
16 changes: 13 additions & 3 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 @@ -11,9 +12,11 @@ import {
NotebookPanel,
NotebookWidgetFactory,
NotebookActions,
Notebook
Notebook,
StaticNotebook
} from '@jupyterlab/notebook';
import { Cell } from '@jupyterlab/cells';
import { MySTOptionsProvider } from './myst';
import { MySTContentFactory } from './MySTContentFactory';

import { ISessionContextDialogs } from '@jupyterlab/apputils';
Expand All @@ -30,18 +33,25 @@ 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.
*/
export const IMySTNotebookOptions = new Token<MySTOptionsProvider<StaticNotebook>>('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: MySTOptionsProvider<StaticNotebook>) => {
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
38 changes: 30 additions & 8 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 @@ -36,6 +36,28 @@ import { imageUrlSourceTransform } from './images';
import { internalLinksPlugin } from './links';
import { addCiteChildrenPlugin } from './citations';

export interface MySTOptions {

parserOptions: Partial<AllOptions>;
}
Copy link
Member

Choose a reason for hiding this comment

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

@fwkoch is there any thinking we need to on this before we start encouraging using these more widely?!

They look pretty solid I think.


export interface MySTOptionsProvider<Widget> {

get(widget: Widget): MySTOptions;
}

export class MySTNotebookDefaults implements MySTOptionsProvider<StaticNotebook> {

get(notebook: StaticNotebook): MySTOptions {
return {
parserOptions: {
directives: [cardDirective, gridDirective, ...tabDirectives],
roles: [evalRole],
},
};
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I would love to see you are overriding things with other extensions @tavin!

Copy link
Contributor Author

Choose a reason for hiding this comment

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


const evalRole: RoleSpec = {
name: 'eval',
body: {
Expand All @@ -48,11 +70,8 @@ const evalRole: RoleSpec = {
}
};

export function markdownParse(text: string): Root {
const mdast = mystParse(text, {
directives: [cardDirective, gridDirective, ...tabDirectives],
roles: [evalRole]
});
export function markdownParse(text: string, options: Partial<AllOptions>): Root {
Copy link
Member

Choose a reason for hiding this comment

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

Should options be potentially undefined here (and other places)?

Suggested change
export function markdownParse(text: string, options: Partial<AllOptions>): Root {
export function markdownParse(text: string, options?: Partial<AllOptions>): Root {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am newish to typescript, actually I didn't know that was available syntax.... but anyway, I don't know? Why?

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 @@ -73,7 +92,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 @@ -87,7 +109,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