Skip to content

Commit 3453f29

Browse files
authored
render jupyter percent scripts (#6700)
1 parent 1fd1d0a commit 3453f29

File tree

10 files changed

+261
-104
lines changed

10 files changed

+261
-104
lines changed

src/core/jupyter/jupyter-fixups.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { stringify } from "yaml/mod.ts";
88
import { warning } from "log/mod.ts";
99

1010
import { kTitle } from "../../config/constants.ts";
11-
import { Metadata } from "../../publish/netlify/api/index.ts";
11+
import { Metadata } from "../../config/types.ts";
1212
import { lines } from "../lib/text.ts";
1313
import { markdownWithExtractedHeading } from "../pandoc/pandoc-partition.ts";
1414
import { partitionYamlFrontMatter, readYamlFromMarkdown } from "../yaml.ts";
@@ -135,17 +135,17 @@ export function fixupBokehCells(nb: JupyterNotebook): JupyterNotebook {
135135
return nb;
136136
}
137137

138-
export function fixupFrontMatter(nb: JupyterNotebook): JupyterNotebook {
139-
// helper to generate yaml
140-
const asYamlText = (yaml: Metadata) => {
141-
return stringify(yaml, {
142-
indent: 2,
143-
lineWidth: -1,
144-
sortKeys: false,
145-
skipInvalid: true,
146-
});
147-
};
138+
// helper to generate yaml
139+
export function asYamlText(yaml: Metadata) {
140+
return stringify(yaml, {
141+
indent: 2,
142+
lineWidth: -1,
143+
sortKeys: false,
144+
skipInvalid: true,
145+
});
146+
}
148147

148+
export function fixupFrontMatter(nb: JupyterNotebook): JupyterNotebook {
149149
// helper to create nb lines (w/ newline after)
150150
const nbLines = (lns: string[]) => {
151151
return lns.map((line) => line.endsWith("\n") ? line : `${line}\n`);

src/core/jupyter/jupyter.ts

Lines changed: 30 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ import {
141141
JupyterToMarkdownResult,
142142
} from "./types.ts";
143143
import { figuresDir, inputFilesDir } from "../render.ts";
144-
import { lines } from "../text.ts";
144+
import { lines, trimEmptyLines } from "../lib/text.ts";
145145
import { partitionYamlFrontMatter, readYamlFromMarkdown } from "../yaml.ts";
146146
import { languagesInMarkdown } from "../../execute/engine-shared.ts";
147147
import {
@@ -347,7 +347,7 @@ export async function quartoMdToJupyter(
347347
delete yaml.jupyter;
348348
// write the cell only if there is metadata to write
349349
if (Object.keys(yaml).length > 0) {
350-
const yamlFrontMatter = mdTrimEmptyLines(lines(stringify(yaml, {
350+
const yamlFrontMatter = trimEmptyLines(lines(stringify(yaml, {
351351
indent: 2,
352352
lineWidth: -1,
353353
sortKeys: false,
@@ -405,7 +405,7 @@ export async function quartoMdToJupyter(
405405
}
406406

407407
// if the source is empty then don't add it
408-
cell.source = mdTrimEmptyLines(cell.source);
408+
cell.source = trimEmptyLines(cell.source);
409409
if (cell.source.length > 0) {
410410
nb.cells.push(cell);
411411
}
@@ -898,7 +898,7 @@ export function jupyterCellOptionsAsComment(
898898
...stringifyOptions,
899899
});
900900
const commentChars = langCommentChars(language);
901-
const yamlOutput = mdTrimEmptyLines(lines(cellYaml)).map((line) => {
901+
const yamlOutput = trimEmptyLines(lines(cellYaml)).map((line) => {
902902
line = optionCommentPrefix(commentChars[0]) + line +
903903
optionCommentSuffix(commentChars[1]);
904904
return line + "\n";
@@ -965,6 +965,26 @@ export function mdFromContentCell(
965965
return contentCellEnvelope(cell.id, mdEnsureTrailingNewline(source));
966966
}
967967

968+
export function mdFormatOutput(format: string, source: string[]) {
969+
const ticks = ticksForCode(source);
970+
return mdEnclosedOutput(ticks + "{=" + format + "}", source, ticks);
971+
}
972+
973+
export function mdRawOutput(mimeType: string, source: string[]) {
974+
switch (mimeType) {
975+
case kTextHtml:
976+
return mdHtmlOutput(source);
977+
case kTextLatex:
978+
return mdLatexOutput(source);
979+
case kRestructuredText:
980+
return mdFormatOutput("rst", source);
981+
case kApplicationRtf:
982+
return mdFormatOutput("rtf", source);
983+
case kApplicationJavascript:
984+
return mdScriptOutput(mimeType, source);
985+
}
986+
}
987+
968988
export function mdFromRawCell(
969989
cell: JupyterCellWithOptions,
970990
options?: JupyterToMarkdownOptions,
@@ -973,17 +993,9 @@ export function mdFromRawCell(
973993

974994
const mimeType = cell.metadata?.[kCellRawMimeType];
975995
if (mimeType) {
976-
switch (mimeType) {
977-
case kTextHtml:
978-
return rawCellEnvelope(cell.id, mdHtmlOutput(cell.source));
979-
case kTextLatex:
980-
return rawCellEnvelope(cell.id, mdLatexOutput(cell.source));
981-
case kRestructuredText:
982-
return rawCellEnvelope(cell.id, mdFormatOutput("rst", cell.source));
983-
case kApplicationRtf:
984-
return rawCellEnvelope(cell.id, mdFormatOutput("rtf", cell.source));
985-
case kApplicationJavascript:
986-
return rawCellEnvelope(cell.id, mdScriptOutput(mimeType, cell.source));
996+
const rawOutput = mdRawOutput(mimeType, cell.source);
997+
if (rawOutput) {
998+
return rawCellEnvelope(cell.id, rawOutput);
987999
}
9881000
}
9891001

@@ -1386,15 +1398,15 @@ async function mdFromCodeCell(
13861398
line.search(/echo:\s+fenced/) === -1
13871399
);
13881400
if (optionsSource.length > 0) {
1389-
source = mdTrimEmptyLines(source, "trailing");
1401+
source = trimEmptyLines(source, "trailing");
13901402
} else {
1391-
source = mdTrimEmptyLines(source, "all");
1403+
source = trimEmptyLines(source, "all");
13921404
}
13931405
source.unshift(...optionsSource);
13941406
source.unshift("```{{" + options.language + "}}\n");
13951407
source.push("\n```\n");
13961408
} else if (cell.optionsSource.length > 0) {
1397-
source = mdTrimEmptyLines(source, "leading");
1409+
source = trimEmptyLines(source, "leading");
13981410
}
13991411
if (options.preserveCodeCellYaml) {
14001412
md.push(...cell.optionsSource);
@@ -1868,11 +1880,6 @@ function mdMarkdownOutput(md: string[]) {
18681880
return md.join("") + "\n";
18691881
}
18701882

1871-
function mdFormatOutput(format: string, source: string[]) {
1872-
const ticks = ticksForCode(source);
1873-
return mdEnclosedOutput(ticks + "{=" + format + "}", source, ticks);
1874-
}
1875-
18761883
function mdLatexOutput(latex: string[]) {
18771884
return mdFormatOutput("tex", latex);
18781885
}
@@ -1902,36 +1909,6 @@ function mdScriptOutput(mimeType: string, script: string[]) {
19021909
return mdHtmlOutput(scriptTag);
19031910
}
19041911

1905-
function mdTrimEmptyLines(
1906-
lines: string[],
1907-
trim: "leading" | "trailing" | "all" = "all",
1908-
) {
1909-
// trim leading lines
1910-
if (trim === "all" || trim === "leading") {
1911-
const firstNonEmpty = lines.findIndex((line) => line.trim().length > 0);
1912-
if (firstNonEmpty === -1) {
1913-
return [];
1914-
}
1915-
lines = lines.slice(firstNonEmpty);
1916-
}
1917-
1918-
// trim trailing lines
1919-
if (trim === "all" || trim === "trailing") {
1920-
let lastNonEmpty = -1;
1921-
for (let i = lines.length - 1; i >= 0; i--) {
1922-
if (lines[i].trim().length > 0) {
1923-
lastNonEmpty = i;
1924-
break;
1925-
}
1926-
}
1927-
if (lastNonEmpty > -1) {
1928-
lines = lines.slice(0, lastNonEmpty + 1);
1929-
}
1930-
}
1931-
1932-
return lines;
1933-
}
1934-
19351912
function mdCodeOutput(code: string[], clz?: string) {
19361913
const ticks = ticksForCode(code);
19371914
const open = ticks + (clz ? `{.${clz}}` : "");

src/core/lib/text.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,36 @@ export function normalizeNewlines(text: string) {
1616
return lines(text).join("\n");
1717
}
1818

19+
export function trimEmptyLines(
20+
lines: string[],
21+
trim: "leading" | "trailing" | "all" = "all",
22+
) {
23+
// trim leading lines
24+
if (trim === "all" || trim === "leading") {
25+
const firstNonEmpty = lines.findIndex((line) => line.trim().length > 0);
26+
if (firstNonEmpty === -1) {
27+
return [];
28+
}
29+
lines = lines.slice(firstNonEmpty);
30+
}
31+
32+
// trim trailing lines
33+
if (trim === "all" || trim === "trailing") {
34+
let lastNonEmpty = -1;
35+
for (let i = lines.length - 1; i >= 0; i--) {
36+
if (lines[i].trim().length > 0) {
37+
lastNonEmpty = i;
38+
break;
39+
}
40+
}
41+
if (lastNonEmpty > -1) {
42+
lines = lines.slice(0, lastNonEmpty + 1);
43+
}
44+
}
45+
46+
return lines;
47+
}
48+
1949
// NB we can't use JS matchAll or replaceAll here because we need to support old
2050
// Chromium in the IDE
2151
//

src/execute/engine.ts

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
/*
2-
* engine.ts
3-
*
4-
* Copyright (C) 2020-2022 Posit Software, PBC
5-
*
6-
*/
2+
* engine.ts
3+
*
4+
* Copyright (C) 2020-2022 Posit Software, PBC
5+
*/
76

87
import { extname, join } from "path/mod.ts";
98

@@ -20,8 +19,8 @@ import { kEngine } from "../config/constants.ts";
2019

2120
import { knitrEngine } from "./rmd.ts";
2221
import { jupyterEngine } from "./jupyter/jupyter.ts";
23-
import { markdownEngine } from "./markdown.ts";
24-
import { ExecutionEngine } from "./types.ts";
22+
import { kMdExtensions, markdownEngine } from "./markdown.ts";
23+
import { ExecutionEngine, kQmdExtensions } from "./types.ts";
2524
import { languagesInMarkdown } from "./engine-shared.ts";
2625
import { languages as handlerLanguages } from "../core/handlers/base.ts";
2726
import { MappedString } from "../core/lib/text-types.ts";
@@ -82,10 +81,7 @@ export function engineValidExtensions(): string[] {
8281
return ld.uniq(kEngines.flatMap((engine) => engine.validExtensions()));
8382
}
8483

85-
export function markdownExecutionEngine(
86-
markdown: string,
87-
flags?: RenderFlags,
88-
) {
84+
export function markdownExecutionEngine(markdown: string, flags?: RenderFlags) {
8985
// read yaml and see if the engine is declared in yaml
9086
// (note that if the file were a non text-file like ipynb
9187
// it would have already been claimed via extension)
@@ -100,9 +96,7 @@ export function markdownExecutionEngine(
10096
return engine;
10197
}
10298
const format = metadataAsFormat(yaml);
103-
if (
104-
format.execute?.[kEngine] === engine.name
105-
) {
99+
if (format.execute?.[kEngine] === engine.name) {
106100
return engine;
107101
}
108102
}
@@ -142,9 +136,7 @@ export function markdownExecutionEngine(
142136
* @param file filename
143137
* @returns the reason
144138
*/
145-
export function fileEngineClaimReason(
146-
file: string,
147-
) {
139+
export function fileEngineClaimReason(file: string) {
148140
// get the extension and validate that it can be handled by at least one of our engines
149141
const ext = extname(file).toLowerCase();
150142
if (!kEngines.some((engine) => engine.validExtensions().includes(ext))) {
@@ -153,7 +145,7 @@ export function fileEngineClaimReason(
153145

154146
// try to find an engine that claims this extension outright
155147
for (const engine of kEngines) {
156-
if (engine.claimsExtension(ext)) {
148+
if (engine.claimsFile(file, ext)) {
157149
return "extension";
158150
}
159151
}
@@ -174,17 +166,21 @@ export function fileExecutionEngine(
174166

175167
// try to find an engine that claims this extension outright
176168
for (const engine of kEngines) {
177-
if (engine.claimsExtension(ext)) {
169+
if (engine.claimsFile(file, ext)) {
178170
return engine;
179171
}
180172
}
181173

182174
// if we were passed a transformed markdown, use that for the text instead
183175
// of the contents of the file.
184-
return markdownExecutionEngine(
185-
markdown ? markdown.value : Deno.readTextFileSync(file),
186-
flags,
187-
);
176+
if (kMdExtensions.includes(ext) || kQmdExtensions.includes(ext)) {
177+
return markdownExecutionEngine(
178+
markdown ? markdown.value : Deno.readTextFileSync(file),
179+
flags,
180+
);
181+
} else {
182+
return undefined;
183+
}
188184
}
189185

190186
export async function fileExecutionEngineAndTarget(

0 commit comments

Comments
 (0)