import { stringify } from "../../core/yaml.ts";
import {
partitionYamlFrontMatter,
readYamlFromMarkdown,
} from "../../core/yaml.ts";
import { kCellId, kCellLabel } from "../../config/constants.ts";
import { JupyterCell, JupyterCellOptions } from "../../core/jupyter/types.ts";
import {
jupyterAutoIdentifier,
jupyterCellOptionsAsComment,
jupyterCellWithOptions,
jupyterFromFile,
mdEnsureTrailingNewline,
mdFromContentCell,
mdFromRawCell,
quartoMdToJupyter,
} from "../../core/jupyter/jupyter.ts";
import { partitionCellOptions } from "../../core/lib/partition-cell-options.ts";
import { Metadata } from "../../config/types.ts";
import { jupyterKernelspec } from "../../core/jupyter/kernels.ts";
import { fixupFrontMatter } from "../../core/jupyter/jupyter-fixups.ts";
import {
jupyterCellSrcAsLines,
jupyterCellSrcAsStr,
} from "../../core/jupyter/jupyter-shared.ts";
import { assert } from "testing/asserts";
import { getEndingNewlineCount } from "../../core/lib/text.ts";
export async function markdownToJupyterNotebook(
file: string,
includeIds: boolean,
) {
const markdown = Deno.readTextFileSync(file);
const notebook = await quartoMdToJupyter(markdown, includeIds);
return JSON.stringify(notebook, null, 2);
}
export async function jupyterNotebookToMarkdown(
file: string,
includeIds: boolean,
) {
const notebook = fixupFrontMatter(jupyterFromFile(file));
let kernelspec = notebook.metadata.kernelspec;
if (
kernelspec.language === undefined && notebook.metadata.language_info?.name
) {
kernelspec = {
...kernelspec,
language: notebook.metadata.language_info?.name,
};
}
if (kernelspec.language === undefined) {
throw new Error(
"No language found in kernelspec for notebook " + file,
);
}
const md: string[] = [];
let frontMatter: string | undefined;
for (let i = 0; i < notebook.cells.length; i++) {
{
const cell = notebook.cells[i];
const cellWithOptions = jupyterCellWithOptions(
i,
kernelspec.language,
cell,
);
const endingNewLineCount = getEndingNewlineCount(md);
if (i > 0 && endingNewLineCount < 2) {
md.push("\n\n");
}
switch (cell.cell_type) {
case "markdown":
md.push(...mdFromContentCell(cellWithOptions));
break;
case "raw":
if (frontMatter === undefined) {
const { yaml: cellYaml, markdown: cellMarkdown } =
partitionYamlFrontMatter(
jupyterCellSrcAsStr(cell),
) || {};
if (cellYaml) {
frontMatter = cellYaml;
}
if (cellMarkdown) {
md.push(cellMarkdown);
}
} else {
md.push(...mdFromRawCell(cellWithOptions));
}
break;
case "code":
md.push(
...(await mdFromCodeCell(
kernelspec.language.toLowerCase(),
cell,
includeIds,
)),
);
break;
default:
throw new Error("Unexpected cell type " + cell.cell_type);
}
if (i > 0 || !frontMatter) {
md.push("\n");
}
}
}
const mdSource = md.join("");
const yaml: Metadata = frontMatter ? readYamlFromMarkdown(frontMatter) : {};
yaml.jupyter = notebook.metadata.jupytext
? {
jupytext: notebook.metadata.jupytext,
kernelspec: notebook.metadata.kernelspec,
}
: notebook.metadata.kernelspec.name;
if (typeof yaml.jupyter === "string") {
if (!await jupyterKernelspec(yaml.jupyter)) {
yaml.jupyter = {
kernelspec: notebook.metadata.kernelspec,
};
}
}
assert(frontMatter || !mdSource.match(/^\n\n/));
const maybeYamlMdBreak = frontMatter ? "" : "\n\n";
const yamlText = stringify(yaml, {
indent: 2,
lineWidth: -1,
sortKeys: false,
skipInvalid: true,
});
return `---\n${yamlText}---${maybeYamlMdBreak}${mdSource}`;
}
async function mdFromCodeCell(
language: string,
cell: JupyterCell,
includeIds: boolean,
) {
if (!cell.source.length) {
return [];
}
const maxBackticks = Math.max(
...jupyterCellSrcAsLines(cell).map((line) =>
line.match(/^`+/g)?.[0].length || 0
),
2,
);
const backticks = "`".repeat(maxBackticks + 1);
const md: string[] = [backticks + "{" + language + "}\n"];
const { yaml, source } = await partitionCellOptions(
language,
jupyterCellSrcAsLines(cell),
);
const options = yaml ? yaml as JupyterCellOptions : {};
if (!includeIds) {
delete cell.id;
delete cell.metadata["id"];
delete cell.metadata["outputId"];
} else {
if (cell.id) {
if (options[kCellLabel]) {
const label = String(options[kCellLabel]);
if (jupyterAutoIdentifier(label) === cell.id) {
cell.id = undefined;
}
}
}
}
let yamlOptions: Metadata = {};
if (cell.id) {
yamlOptions[kCellId] = cell.id;
}
yamlOptions = {
...cell.metadata,
...yaml,
...yamlOptions,
};
if (yamlOptions[kCellId]) {
md.push(
...jupyterCellOptionsAsComment(language, { id: yamlOptions[kCellId] }),
);
delete yamlOptions[kCellId];
}
if (yaml) {
const yamlOutput: Metadata = {};
for (const key in yaml) {
const value = yamlOptions[key];
if (value !== undefined) {
yamlOutput[key] = value;
delete yamlOptions[key];
}
}
md.push(...jupyterCellOptionsAsComment(language, yamlOutput));
}
const metadataOutput: Metadata = {};
for (const key in cell.metadata) {
const value = cell.metadata[key];
if (value !== undefined) {
metadataOutput[key] = value;
delete yamlOptions[key];
}
}
md.push(
...jupyterCellOptionsAsComment(language, metadataOutput, { flowLevel: 1 }),
);
md.push(...mdEnsureTrailingNewline(source));
md.push(backticks + "\n");
return md;
}