Skip to content

Commit 16b652d

Browse files
committed
Merge branch 'main' of github.com:quarto-dev/quarto-cli
2 parents 20d1912 + 441cd9b commit 16b652d

File tree

7 files changed

+298
-109
lines changed

7 files changed

+298
-109
lines changed

design/TODO.txt

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,44 +12,23 @@ Markdown Serialization
1212

1313
JJ:
1414

15-
- convert to/from text vs. ipynb representation
16-
- notebook metadata:
17-
- dump into 'jupyter' front matter key on way to markdown
18-
- 'jupyter' front matter goes into notebook metadata
19-
- 'notebook', 'markdown' as convert targets,
20-
21-
- dev server preview doesn't drive to last rendered
22-
- books-crossrefs.ts#127 - fixing up heading links not working properly
2315
- TODO: measure lag at the beginning of project single file render
16+
- make sure that crossrefs are appropriately disabled for unumbered chapters
17+
- More opinionated <kbd>
18+
- can we make section-numbers work more sensibly with H2 (chapter-level). --top-level-division, --shift-heading-level
2419

2520

2621
CT:
2722
- search highlighting like is done in mastering-shiny
2823
(https://mastering-shiny.org/performance.html?q=plot#caching-plots)
2924
jumping between highlighted terms (via keyboard or affordance at right or top)
3025

26+
- Better treatment for code blocks within tabs (just use a thin border, could
27+
consider background color)
3128

3229

33-
books
34-
=====================================================================
35-
36-
- make sure that crossrefs are appropriately disabled for unumbered chapters
37-
- More opinionated <kbd>
38-
39-
- can we make section-numbers work more sensibly with H2 (chapter-level). --top-level-division, --shift-heading-level
40-
41-
- footnotes:
42-
(1) do we have consistent behavior across formats
43-
(2) can we provide options for per-chapter vs. global
4430

4531

46-
quarto-python dev workflow:
47-
48-
```bash
49-
$ python3 -m venv .venv
50-
$ source .venv/bin/activate
51-
$ pip install -e ./
52-
```
5332

5433
TODO
5534
=====================================================================
@@ -78,6 +57,16 @@ TODO: convert to css grid for columns (allows for content floating and crossing
7857

7958

8059

60+
61+
quarto-python dev workflow:
62+
63+
```bash
64+
$ python3 -m venv .venv
65+
$ source .venv/bin/activate
66+
$ pip install -e ./
67+
```
68+
69+
8170
Installation/Compilation
8271
=====================================================================
8372

@@ -107,6 +96,11 @@ JupyterLab
10796

10897
- webshot equivalent for printed output
10998

99+
100+
- footnotes:
101+
(1) do we have consistent behavior across formats
102+
(2) can we provide options for per-chapter vs. global
103+
110104
- Bibliography per chapter
111105

112106
- Glossaries (https://jupyterbook.org/content/content-blocks.html#glossaries)

src/command/convert/cmd.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const convertCommand = new Command()
2626
"Convert between markdown and notebook representations of documents.",
2727
)
2828
.option(
29-
"-t, --to <format:string>",
29+
"-t, --to [format:string]",
3030
"Format to convert to (markdown or notebook)",
3131
)
3232
.option(
@@ -65,8 +65,10 @@ export const convertCommand = new Command()
6565
: kMarkdownFormat;
6666

6767
// determine and validate target format
68-
const targetFormat = options.to ||
69-
(srcFormat === kNotebookFormat ? kMarkdownFormat : kNotebookFormat);
68+
const targetFormat = options.to;
69+
if (!targetFormat) {
70+
throw new Error("Target format (--to) not specified");
71+
}
7072
if (![kNotebookFormat, kMarkdownFormat].includes(targetFormat)) {
7173
throw new Error("Invalid target format: " + targetFormat);
7274
}
@@ -82,7 +84,7 @@ export const convertCommand = new Command()
8284

8385
// perform conversion
8486
const converted = srcFormat === kNotebookFormat
85-
? convertNotebookToMarkdown(path)
87+
? convertNotebookToMarkdown(path, includeIds)
8688
: await convertMarkdownToNotebook(path, includeIds);
8789

8890
// write output

src/command/convert/convert.ts

Lines changed: 149 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,165 @@
55
*
66
*/
77

8+
import { stringify } from "encoding/yaml.ts";
9+
10+
import {
11+
partitionYamlFrontMatter,
12+
readYamlFromMarkdown,
13+
} from "../../core/yaml.ts";
814
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,
1026
quartoMdToJupyter,
1127
} from "../../core/jupyter/jupyter.ts";
28+
import { Metadata } from "../../config/metadata.ts";
1229

1330
export async function convertMarkdownToNotebook(
1431
file: string,
1532
includeIds: boolean,
1633
) {
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);
2435
return JSON.stringify(notebook, null, 2);
2536
}
2637

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");
31167

32-
return "";
168+
return md;
33169
}

src/command/serve/watch.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export function watchProject(
6666

6767
// lib dir
6868
const libDirConfig = project.config?.project[kProjectLibDir];
69+
const libDirSource = libDirConfig
70+
? join(project.dir, libDirConfig)
71+
: undefined;
6972
const libDir = libDirConfig ? join(outputDir, libDirConfig) : undefined;
7073

7174
// if any of the paths are in the output dir (but not the lib dir) then return true
@@ -84,6 +87,10 @@ export function watchProject(
8487

8588
// is this a resource file?
8689
const isResourceFile = (path: string) => {
90+
// exclude libdir
91+
if (libDirSource && path.startsWith(libDirSource)) {
92+
return false;
93+
}
8794
if (renderResult) {
8895
if (project.files.resources?.includes(path)) {
8996
return true;

0 commit comments

Comments
 (0)