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

🤩 Inline expressions #92

Merged
merged 9 commits into from
Feb 21, 2023
Merged

🤩 Inline expressions #92

merged 9 commits into from
Feb 21, 2023

Conversation

rowanc1
Copy link
Member

@rowanc1 rowanc1 commented Feb 20, 2023

This PR add the ability to have inline expressions, and is largely based off work by @agoose77 in jupyterlab-imarkdown, see #72.

The bulk of this is working, but we should add the textRendererFactory to improve inline styles. The mime_bundle is stored in options and can be re-rendered without the kernel, which is important for other applications like MyST Websites.

The inline expressions work, can be both with simple evaluations, inline widgets, or spark-lines using matplotlib.

inline-expressions

cookies

stock-price

@github-actions
Copy link
Contributor

Binder 👈 Launch a Binder on branch executablebooks/jupyterlab-myst/feat/inline-expressions

Base automatically changed from feat/myst-styles to main February 20, 2023 16:10
@rowanc1 rowanc1 force-pushed the feat/inline-expressions branch from cbfc114 to 6215bbd Compare February 20, 2023 16:10
@rowanc1 rowanc1 added the enhancement New feature or request label Feb 20, 2023
Copy link
Member Author

@rowanc1 rowanc1 left a comment

Choose a reason for hiding this comment

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

Annotated review of what is going on!

super(options);

this.__rendermime = options.rendermime.clone();
// this.__rendermime.addFactory(textRendererFactory);
Copy link
Member Author

Choose a reason for hiding this comment

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

We should add or improve the inline rendering.

Copy link
Member Author

Choose a reason for hiding this comment

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

@agoose77, looking more at the text renderer -- I think we might actually just want to do this in react even?

I think we are going to have to have a bunch of custom renderers, that are specific to the inline execution, but that can probably all come later!

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, so before defering to the standard rendermime renderers, take a different rendering route based on mimetype and skip the defaults altogether, for text etc...

For other things like widgets, we can maybe look at redefining/overriding the styles in CSS? we'd have to figure out how that plays with the style attributes on widgets themselves. https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Styling.html#The-style-attribute

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think rendering plain-text in MyST might be reasonable. There's a big question over MyST integration in JLab as to how much we take over e.g. Markdown rendering currently goes through the default marked implementation.

Perhaps we want to recognise a particular flavour of Markdown, so that users can opt-in to displaying rich text via outputs. Also, I wonder what myst-nb does here — how do text/markdown outputs get rendered (as MyST?)

Copy link
Collaborator

Choose a reason for hiding this comment

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

The downside of this is now other JupyterLab extensions can't override e.g. text/plain rendering, but it's not necessarily a problem; we just need to define what should be the behaviour here. My take is that we special-case text/plain, errors, LaTeX, and Markdown (maybe only if it's MyST markdown), and the rest goes through the rendermime interface.

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice, agree with all of that! Will pick it up in a future PR!


export class MySTMarkdownCell
extends MarkdownCell
implements IMySTMarkdownCell
{
private _doneRendering = new PromiseDelegate<void>();
Copy link
Member Author

Choose a reason for hiding this comment

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

There is now a promise for when the initial mdast parse is complete for the cell, then this can trigger the inline execution.

Comment on lines +80 to +81
const expressions = selectAll('inlineExpression', mdast);
return expressions.map(node => (node as any).value);
Copy link
Member Author

Choose a reason for hiding this comment

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

Gather all of the expressions to be evaluated. This is just the body of the role.

Copy link
Collaborator

Choose a reason for hiding this comment

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

So much nicer.

Copy link
Member Author

Choose a reason for hiding this comment

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

Any thoughts on what this should be called on the spec side for the actual node? We could also have it code with an executable flag on it?

That is closer to what JATS does.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Assuming that we're referring to inlineExpression, I'd probably prefer e.g. expr or expression?

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess there is some precedence in the spec at the moment.

https://myst-tools.org/docs/spec/commonmark#inline-code

I am inclined to leave it as is in this PR, and revisit when we start to standardize this in the spec?! My thinking is that we may want a difference in inline vs block-level expressions. e.g. an interactive figure inline maybe has a different renderer than the block level renderer. That could mean different mimebundles in the future.

These are all ephemeral at the moment, so it shouldn't matter if we want to change it!

Comment on lines 106 to 110
<CellMetadataProvider
metadata={metadata}
trusted={this.model.trusted}
rendermime={this.__rendermime}
>
Copy link
Member Author

Choose a reason for hiding this comment

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

We add a cell metadata provider to give access to these values in the inline renderers.

Comment on lines +50 to +53
const content: KernelMessage.IExecuteRequestMsg['content'] = {
code: '',
user_expressions: userExpressions
};
Copy link
Member Author

Choose a reason for hiding this comment

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

user_expressions are a Jupyter thing, that map is provided and executed.

Comment on lines +127 to +128
const executor: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-myst:executor',
Copy link
Member Author

Choose a reason for hiding this comment

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

We add another plugin to allow markdown cells to be executed.

Copy link
Contributor

Choose a reason for hiding this comment

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

so 2 plugins are installed now? and the both work independently?
i.e. the other plugin takes care of Markdown cell rendering whilst this one takes care of execution post render? or are we also rendering here when there is an inline expression?

Copy link
Collaborator

@agoose77 agoose77 Feb 21, 2023

Choose a reason for hiding this comment

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

A single JupyterLab extension can install multiple plugins. Here we provide the "execute inline expressions and store results" using this :executor plugin. The renderering is still exclusively performed by the MySTMarkdownCell work.

Copy link
Member Author

Choose a reason for hiding this comment

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

It does mean that you can turn parts of it off at a time, which is quite nice!

}
}

export function InlineRenderer({ value }: { value?: string }): JSX.Element {
Copy link
Member Author

Choose a reason for hiding this comment

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

This react component gets attached in the inlineExpression for mdast. We have to be careful here to create the rendermime one time, and then update it when the expression result changes. There is also ways to dispose of the renderer when this component gets removed.

Copy link
Member Author

Choose a reason for hiding this comment

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

@stevejpurves you might have comments here from your experience working with react and thebe?!

Copy link
Contributor

@stevejpurves stevejpurves Feb 21, 2023

Choose a reason for hiding this comment

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

this is ok especially if it works. I have had issues with render passes previously before a ref was resolved and solved that by using "callback refs" to only attach to the dom once the ref was available. I think that is more of an issue in components with conditional rendering -- which this one has i.e. when there is no expressionResult but if this pattern works and we reliably get the output attached to the dom, then no need to change it.

Comment on lines +37 to +47
const evalRole: RoleSpec = {
name: 'eval',
body: {
type: ParseTypesEnum.string,
required: true
},
run(data: RoleData): GenericNode[] {
const value = data.body as string;
return [{ type: 'inlineExpression', value }];
}
};
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the role spec!

@@ -125,4 +147,6 @@ export function parseContent(notebook: StaticNotebook): void {
cell.myst.post = mdast.children[index];
cell.mystRender();
});

return Promise.all(promises).then(() => undefined);
Copy link
Member Author

Choose a reason for hiding this comment

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

This content now waits on the render, and then returns so that execution can happen. We must parse the cells BEFORE we execute.

Comment on lines +10 to +12
inlineExpression: (node, children) => {
return <InlineRenderer value={node.value} />;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the new myst renderer for the node.

@rowanc1

This comment was marked as outdated.


this.__rendermime = options.rendermime.clone();
// Note we cannot clone this, and it must be the parents (the notebooks)
this.__rendermime = parent.rendermime;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah! I didn't get this far in my reading, nice spot.

Copy link
Member Author

Choose a reason for hiding this comment

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

I am not totally sure why this happens, I feel like the clone should work!?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't break anything on my local test build, but I don't think we should be cloning in any case. I think I probably did that in jupyterlab-imarkdown in the very early work and never noticed it. We want to use the same rendermime registry as passed in:

Suggested change
this.__rendermime = parent.rendermime;
this.__rendermime = options.rendermime;

src/MySTMarkdownCell.tsx Outdated Show resolved Hide resolved
Comment on lines +80 to +81
const expressions = selectAll('inlineExpression', mdast);
return expressions.map(node => (node as any).value);
Copy link
Collaborator

Choose a reason for hiding this comment

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

So much nicer.

@rowanc1 rowanc1 marked this pull request as ready for review February 20, 2023 21:55
README.md Outdated Show resolved Hide resolved
Co-authored-by: Angus Hollands <goosey15@gmail.com>
@rowanc1
Copy link
Member Author

rowanc1 commented Feb 20, 2023

Was going to share the binder link -- but we don't have matplotlib or numpy on it!

Errors are pretty great though!
image

@rowanc1
Copy link
Member Author

rowanc1 commented Feb 20, 2023

Binder is now working well!!


export function InlineRenderer({ value }: { value?: string }): JSX.Element {
const ref = useRef<HTMLDivElement>(null);
const { metadata, trusted, rendermime } = useCellMetadata();
Copy link
Contributor

Choose a reason for hiding this comment

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

I find the hook name confusing, as it makes me think e are reading the rendermime from metadata when its an runtime object coming from the notebook/tracker

Copy link
Member Author

Choose a reason for hiding this comment

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

I have changed this to a cellProvider, which ... provides the cell! :)

import { NodeRenderer } from '@myst-theme/providers';
import { InlineRenderer } from './inlineExpression';

export const renderers: Record<string, NodeRenderer> = {
Copy link
Contributor

Choose a reason for hiding this comment

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

there are a lot of renderers flying around! these are mystRenderers specifically here though it seems

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I think there is a wider clean up for the whole repo as we start to get a bit more complicated roles and parsing structure in this repo.

I am inclined to kick this down the road though!

@rowanc1
Copy link
Member Author

rowanc1 commented Feb 21, 2023

Ok, @agoose77 and @stevejpurves. I think this PR is ready to come in!

We will have another wuick PR on styles later today, and then aim to release as 1.1.0? or maybe 1.0.2? Feels more minor than patch?

@agoose77
Copy link
Collaborator

Minor, agreed!

@rowanc1
Copy link
Member Author

rowanc1 commented Feb 21, 2023

In it goes!!

@rowanc1 rowanc1 merged commit 270dc61 into main Feb 21, 2023
@rowanc1 rowanc1 deleted the feat/inline-expressions branch February 21, 2023 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants