|
5 | 5 | * |
6 | 6 | */ |
7 | 7 |
|
| 8 | +import { stringify } from "encoding/yaml.ts"; |
| 9 | + |
| 10 | +import { |
| 11 | + partitionYamlFrontMatter, |
| 12 | + readYamlFromMarkdown, |
| 13 | +} from "../../core/yaml.ts"; |
8 | 14 | import { |
9 | | - jupyterKernelspecFromFile, |
| 15 | + jupyterAutoIdentifier, |
| 16 | + JupyterCell, |
| 17 | + JupyterCellOptions, |
| 18 | + jupyterCellOptionsAsComment, |
| 19 | + jupyterFromFile, |
| 20 | + kCellId, |
| 21 | + kCellLabel, |
| 22 | + mdEnsureTrailingNewline, |
| 23 | + mdFromContentCell, |
| 24 | + mdFromRawCell, |
| 25 | + partitionJupyterCellOptions, |
10 | 26 | quartoMdToJupyter, |
11 | 27 | } from "../../core/jupyter/jupyter.ts"; |
| 28 | +import { Metadata } from "../../config/metadata.ts"; |
12 | 29 |
|
13 | 30 | export async function convertMarkdownToNotebook( |
14 | 31 | file: string, |
15 | 32 | includeIds: boolean, |
16 | 33 | ) { |
17 | | - const [kernelspec, metadata] = await jupyterKernelspecFromFile(file); |
18 | | - const notebook = quartoMdToJupyter( |
19 | | - file, |
20 | | - kernelspec, |
21 | | - metadata, |
22 | | - includeIds, |
23 | | - ); |
| 34 | + const notebook = await quartoMdToJupyter(file, includeIds); |
24 | 35 | return JSON.stringify(notebook, null, 2); |
25 | 36 | } |
26 | 37 |
|
27 | | -export function convertNotebookToMarkdown(file: string) { |
28 | | - // TODO: when converting from notebook to markdown, we do carry any id we find into metadata, however if the |
29 | | - // id matches the auto-converted label then we don't include id |
30 | | - // (could be command line flags to eliminate ids) |
| 38 | +export function convertNotebookToMarkdown(file: string, includeIds: boolean) { |
| 39 | + // read notebook & alias kernelspec |
| 40 | + const notebook = jupyterFromFile(file); |
| 41 | + const kernelspec = notebook.metadata.kernelspec; |
| 42 | + |
| 43 | + // generate markdown |
| 44 | + const md: string[] = []; |
| 45 | + |
| 46 | + for (let i = 0; i < notebook.cells.length; i++) { |
| 47 | + { |
| 48 | + // alias cell |
| 49 | + const cell = notebook.cells[i]; |
| 50 | + |
| 51 | + // write markdown |
| 52 | + switch (cell.cell_type) { |
| 53 | + case "markdown": |
| 54 | + md.push(...mdFromContentCell(cell)); |
| 55 | + break; |
| 56 | + case "raw": |
| 57 | + md.push(...mdFromRawCell(cell)); |
| 58 | + break; |
| 59 | + case "code": |
| 60 | + md.push(...mdFromCodeCell(kernelspec.language, cell, includeIds)); |
| 61 | + break; |
| 62 | + default: |
| 63 | + throw new Error("Unexpected cell type " + cell.cell_type); |
| 64 | + } |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + // join into source |
| 69 | + const mdSource = md.join(""); |
| 70 | + |
| 71 | + // add jupyter kernelspec to front-matter |
| 72 | + const partitioned = partitionYamlFrontMatter(mdSource); |
| 73 | + if (partitioned?.yaml) { |
| 74 | + const yaml = readYamlFromMarkdown(partitioned.yaml); |
| 75 | + yaml.jupyter = notebook.metadata; |
| 76 | + const yamlText = stringify(yaml, { |
| 77 | + indent: 2, |
| 78 | + sortKeys: false, |
| 79 | + skipInvalid: true, |
| 80 | + }); |
| 81 | + return `---\n${yamlText}---\n${partitioned.markdown}\n`; |
| 82 | + } else { |
| 83 | + return mdSource; |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +function mdFromCodeCell( |
| 88 | + language: string, |
| 89 | + cell: JupyterCell, |
| 90 | + includeIds: boolean, |
| 91 | +) { |
| 92 | + // redact if the cell has no source |
| 93 | + if (!cell.source.length) { |
| 94 | + return []; |
| 95 | + } |
| 96 | + |
| 97 | + // begin code cell |
| 98 | + const md: string[] = ["```{" + language + "}\n"]; |
| 99 | + |
| 100 | + // partition |
| 101 | + const { yaml, source } = partitionJupyterCellOptions(language, cell.source); |
| 102 | + const options = yaml ? yaml as JupyterCellOptions : {}; |
| 103 | + console.log(options); |
| 104 | + |
| 105 | + // handle id |
| 106 | + if (cell.id) { |
| 107 | + if (!includeIds) { |
| 108 | + cell.id = undefined; |
| 109 | + } else if (options[kCellLabel]) { |
| 110 | + const label = String(options[kCellLabel]); |
| 111 | + if (jupyterAutoIdentifier(label) === cell.id) { |
| 112 | + cell.id = undefined; |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + // prepare the options for writing |
| 118 | + let yamlOptions: Metadata = {}; |
| 119 | + if (cell.id) { |
| 120 | + yamlOptions[kCellId] = cell.id; |
| 121 | + } |
| 122 | + yamlOptions = { |
| 123 | + ...cell.metadata, |
| 124 | + ...yaml, |
| 125 | + ...yamlOptions, |
| 126 | + }; |
| 127 | + |
| 128 | + // cell id first |
| 129 | + if (yamlOptions[kCellId]) { |
| 130 | + md.push( |
| 131 | + ...jupyterCellOptionsAsComment(language, { id: yamlOptions[kCellId] }), |
| 132 | + ); |
| 133 | + delete yamlOptions[kCellId]; |
| 134 | + } |
| 135 | + |
| 136 | + // yaml |
| 137 | + if (yaml) { |
| 138 | + const yamlOutput: Metadata = {}; |
| 139 | + for (const key in yaml) { |
| 140 | + const value = yamlOptions[key]; |
| 141 | + if (value !== undefined) { |
| 142 | + yamlOutput[key] = value; |
| 143 | + delete yamlOptions[key]; |
| 144 | + } |
| 145 | + } |
| 146 | + md.push(...jupyterCellOptionsAsComment(language, yamlOutput)); |
| 147 | + } |
| 148 | + |
| 149 | + // metadata |
| 150 | + const metadataOutput: Metadata = {}; |
| 151 | + for (const key in cell.metadata) { |
| 152 | + const value = cell.metadata[key]; |
| 153 | + if (value !== undefined) { |
| 154 | + metadataOutput[key] = value; |
| 155 | + delete yamlOptions[key]; |
| 156 | + } |
| 157 | + } |
| 158 | + md.push( |
| 159 | + ...jupyterCellOptionsAsComment(language, metadataOutput, { flowLevel: 1 }), |
| 160 | + ); |
| 161 | + |
| 162 | + // write cell code |
| 163 | + md.push(...mdEnsureTrailingNewline(source)); |
| 164 | + |
| 165 | + // end code cell |
| 166 | + md.push("```\n"); |
31 | 167 |
|
32 | | - return ""; |
| 168 | + return md; |
33 | 169 | } |
0 commit comments