Skip to content
Open
5 changes: 5 additions & 0 deletions .changeset/weak-bananas-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"myst-cli": patch
---

Move kernel execution transform earlier in pipeline
10 changes: 6 additions & 4 deletions packages/myst-cli/src/process/mdast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import {
transformLinkedDOIs,
transformLinkedRORs,
transformOutputsToCache,
transformRenderInlineExpressions,
transformThumbnail,
StaticFileTransformer,
propagateBlockDataToCode,
Expand All @@ -67,6 +66,7 @@ import {
transformFilterOutputStreams,
transformLiftCodeBlocksInJupytext,
transformMystXRefs,
transformLiftExecutionResults,
} from '../transforms/index.js';
import type { ImageExtensions } from '../utils/resolveExtension.js';
import { logMessagesFromVFile } from '../utils/logging.js';
Expand Down Expand Up @@ -194,6 +194,11 @@ export async function transformMdast(
log: session.log,
});
}
await transformOutputsToCache(session, mdast, kind, { minifyMaxCharacters });
await transformLiftExecutionResults(session, mdast, vfile, {
parseMyst: (content: string) => parseMyst(session, content, file),
});
transformFilterOutputStreams(mdast, vfile, frontmatter.settings);

const pipe = unified()
.use(reconstructHtmlPlugin) // We need to group and link the HTML first
Expand Down Expand Up @@ -238,9 +243,6 @@ export async function transformMdast(
// Combine file-specific citation renderers with project renderers from bib files
const fileCitationRenderer = combineCitationRenderers(cache, ...rendererFiles);

transformRenderInlineExpressions(mdast, vfile);
await transformOutputsToCache(session, mdast, kind, { minifyMaxCharacters });
transformFilterOutputStreams(mdast, vfile, frontmatter.settings);
transformCitations(session, file, mdast, fileCitationRenderer, references);
await unified()
.use(codePlugin, { lang: frontmatter?.kernelspec?.language })
Expand Down
25 changes: 23 additions & 2 deletions packages/myst-cli/src/process/notebook.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NotebookCell, RuleId, fileWarn } from 'myst-common';
import type { GenericNode, GenericParent } from 'myst-common';

Check failure on line 2 in packages/myst-cli/src/process/notebook.ts

View workflow job for this annotation

GitHub Actions / lint

'myst-common' imported multiple times
import { selectAll } from 'unist-util-select';
import { nanoid } from 'nanoid';
import type {
Expand All @@ -17,8 +17,7 @@
import { BASE64_HEADER_SPLIT } from '../transforms/images.js';
import { parseMyst } from './myst.js';
import type { Code, InlineExpression } from 'myst-spec-ext';
import type { IUserExpressionMetadata } from '../transforms/inlineExpressions.js';
import { findExpression, metadataSection } from '../transforms/inlineExpressions.js';
import type { IExpressionResult } from 'myst-common';

Check failure on line 20 in packages/myst-cli/src/process/notebook.ts

View workflow job for this annotation

GitHub Actions / lint

'myst-common' imported multiple times
import { frontmatterValidationOpts } from '../frontmatter.js';

import { filterKeys } from 'simple-validators';
Expand All @@ -34,6 +33,28 @@
return { type: 'block', kind, data: JSON.parse(JSON.stringify(cell.metadata)), children };
}

/*
* Where to find user expressions stored in Jupyter Notebook metadata
* This derives from jupyterlab-myst
*/
export const metadataSection = 'user_expressions';

export interface IUserExpressionMetadata {
expression: string;
result: IExpressionResult;
}

export interface IUserExpressionsMetadata {
[metadataSection]: IUserExpressionMetadata[];
}

export function findExpression(
expressions: IUserExpressionMetadata[] | undefined,
value: string,
): IUserExpressionMetadata | undefined {
return expressions?.find((expr) => expr.expression === value);
}

/**
* mdast transform to move base64 cell attachments directly to image nodes
*
Expand Down
2 changes: 1 addition & 1 deletion packages/myst-cli/src/transforms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export * from './include.js';
export * from './links.js';
export * from './mdast.js';
export * from './outputs.js';
export * from './inlineExpressions.js';
export * from './rendermime.js';
export * from './types.js';
92 changes: 0 additions & 92 deletions packages/myst-cli/src/transforms/inlineExpressions.ts

This file was deleted.

138 changes: 133 additions & 5 deletions packages/myst-cli/src/transforms/outputs.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import fs from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { computeHash } from 'myst-cli-utils';
import type { Image, SourceFileKind, Output } from 'myst-spec-ext';
import type { Image, SourceFileKind, Output, InlineExpression } from 'myst-spec-ext';
import { liftChildren, fileError, RuleId, fileWarn } from 'myst-common';
import type { GenericNode, GenericParent } from 'myst-common';
import type { GenericNode, GenericParent, IExpressionResult } from 'myst-common';
import type { ProjectSettings } from 'myst-frontmatter';
import { htmlTransform } from 'myst-transforms';
import stripAnsi from 'strip-ansi';
import { remove } from 'unist-util-remove';
import { selectAll } from 'unist-util-select';
import type { VFile } from 'vfile';
import type { IOutput, IStream } from '@jupyterlab/nbformat';
import type { MinifiedContent, MinifiedOutput, MinifiedMimeOutput } from 'nbtx';
import { ensureString, extFromMimeType, minifyCellOutput, walkOutputs } from 'nbtx';
import type { IOutput, IMimeBundle } from '@jupyterlab/nbformat';
import type { MinifiedContent } from 'nbtx';
import {
ensureString,
extFromMimeType,
minifyCellOutput,
walkOutputs,
convertToIOutputs,
} from 'nbtx';
import { castSession } from '../session/cache.js';
import type { ISession } from '../session/types.js';
import { resolveOutputPath } from './images.js';
import type { StaticPhrasingContent, PhrasingContent } from 'myst-spec';
import type { MystParser, MimeRenderer } from './rendermime.js';
import { MIME_RENDERERS } from './rendermime.js';

function getFilename(hash: string, contentType: string) {
return `${hash}${extFromMimeType(contentType)}`;
Expand All @@ -25,6 +34,125 @@ function getWriteDestination(hash: string, contentType: string, writeFolder: str
return join(writeFolder, getFilename(hash, contentType));
}

async function renderExpression(
node: InlineExpression,
file: VFile,
opts: LiftOptions,
): Promise<PhrasingContent[]> {
const result = node.result as IExpressionResult;
if (result?.status !== 'ok') {
return [];
}
const mimeTypes = [...Object.keys(result.data)];
for (const renderer of MIME_RENDERERS) {
const preferredMime = renderer.renders(mimeTypes);
if (!preferredMime) {
continue;
}
return await renderer.renderPhrasing(
preferredMime,
result.data[preferredMime],
file,
opts.parseMyst,
{
stripQuotes: (result.metadata?.['strip-quotes'] as any) ?? true,
},
);
}
fileWarn(file, 'Unrecognized mime bundle for inline content', {
node,
ruleId: RuleId.inlineExpressionRenders,
});
return [];
}

export interface LiftOptions {
parseMyst: MystParser;
}

/**
* Lift inline expressions from display data to AST nodes
*/
export async function liftExpressions(mdast: GenericParent, file: VFile, opts: LiftOptions) {
const inlineNodes = selectAll('inlineExpression', mdast) as InlineExpression[];
for (const inlineExpression of inlineNodes) {
if (!inlineExpression.result) {
continue;
}
inlineExpression.children = (await renderExpression(
inlineExpression,
file,
opts,
)) as StaticPhrasingContent[];
}
}

/**
* Lift inline expressions from display data to AST nodes
*/
export async function liftOutputs(
session: ISession,
mdast: GenericParent,
vfile: VFile,
opts: LiftOptions,
) {
const cache = castSession(session);
for (const output of selectAll('output', mdast)) {
const jupyter_output = (output as any).jupyter_data;

// Do we have a MIME bundle?
switch (jupyter_output.output_type) {
case 'error': {
break;
}

case 'stream': {
break;
}
case 'execute_result':
case 'update_display_data':
case 'display_data': {
const [properOutput] = convertToIOutputs([jupyter_output], cache.$outputs);
// Find the preferred mime type
const mimeTypes = [...Object.keys(properOutput.data as IMimeBundle)];
const preferredMimeRenderer = MIME_RENDERERS.map(
(renderer): [string | undefined, MimeRenderer] => [renderer.renders(mimeTypes), renderer],
).find(([mimeType]) => mimeType !== undefined);

// If we don't need to process any of these MIME types, skip.
if (preferredMimeRenderer === undefined) {
break;
}
const [contentType, renderer] = preferredMimeRenderer;

// Pull content from cache if minified
const content = (properOutput.data as IMimeBundle)[contentType!];

(output as any).children = await renderer.renderBlock(
contentType!,
content,
vfile,
opts.parseMyst,
{
stripQuotes: (jupyter_output.metadata?.['strip-quotes'] as any) ?? true,
},
);
break;
}
}
}
}

export async function transformLiftExecutionResults(
session: ISession,
mdast: GenericParent,
vfile: VFile,
opts: LiftOptions,
) {
await liftOutputs(session, mdast, vfile, opts);
await liftExpressions(mdast, vfile, opts);
}

/**
* Traverse all output nodes, minify their content, and cache on the session
*/
Expand Down
Loading
Loading