Skip to content

Commit 9a8bbdc

Browse files
committed
Merge branch 'main' of github.com:quarto-dev/quarto-cli into main
2 parents b2323dc + 5e14703 commit 9a8bbdc

File tree

7 files changed

+196
-16
lines changed

7 files changed

+196
-16
lines changed

package/src/import_map.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"observablehq/parser": "https://cdn.skypack.dev/@observablehq/[email protected]",
2323
"events/": "https://deno.land/x/[email protected]/",
2424
"xmlp/": "https://deno.land/x/[email protected]/",
25+
"ansi_up": "https://cdn.skypack.dev/[email protected]",
2526
"ajv": "https://cdn.skypack.dev/[email protected]",
2627
"blueimpMd5": "https://cdn.skypack.dev/[email protected]",
2728
"diff": "https://cdn.skypack.dev/[email protected]",

src/core/ansi-colors.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* ansi-colors.ts
3+
*
4+
* Copyright (C) 2020 by RStudio, PBC
5+
*
6+
*/
7+
8+
import ansiUp from "ansi_up";
9+
import { Element, parseHtml } from "./deno-dom.ts";
10+
11+
export function hasAnsiEscapeCodes(str: string) {
12+
// deno-lint-ignore no-control-regex
13+
return !!str.match(/\x1b\[\d+(?:;\d+)*m/);
14+
}
15+
16+
export async function convertToHtmlSpans(str: string) {
17+
const a = new ansiUp();
18+
a.use_classes = true;
19+
const html = a.ansi_to_html(str);
20+
21+
// wrap in pre so deno-dom preserves whitespace
22+
const doc = await parseHtml(`<pre>${html}</pre>`);
23+
for (const node of doc.querySelectorAll("span")) {
24+
const el = node as Element;
25+
if (el.getAttribute("style") === "font-weight:bold") {
26+
el.removeAttribute("style");
27+
el.classList.add("ansi-bold");
28+
}
29+
}
30+
return doc.querySelector("pre")!.innerHTML;
31+
}

src/core/jupyter/jupyter.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ import { lines } from "../text.ts";
144144
import { readYamlFromMarkdown } from "../yaml.ts";
145145
import { languagesInMarkdown } from "../../execute/engine-shared.ts";
146146
import { pathWithForwardSlashes } from "../path.ts";
147+
import { convertToHtmlSpans, hasAnsiEscapeCodes } from "../ansi-colors.ts";
147148

148149
export const kJupyterNotebookExtensions = [
149150
".ipynb",
@@ -581,10 +582,10 @@ export function jupyterAssets(input: string, to?: string) {
581582
};
582583
}
583584

584-
export function jupyterToMarkdown(
585+
export async function jupyterToMarkdown(
585586
nb: JupyterNotebook,
586587
options: JupyterToMarkdownOptions,
587-
): JupyterToMarkdownResult {
588+
): Promise<JupyterToMarkdownResult> {
588589
// optional content injection / html preservation for html output
589590
// that isn't an ipynb
590591
const isHtml = options.toHtml && !options.toIpynb;
@@ -638,7 +639,7 @@ export function jupyterToMarkdown(
638639
md.push(...mdFromRawCell(cell));
639640
break;
640641
case "code":
641-
md.push(...mdFromCodeCell(cell, ++codeCellIndex, options));
642+
md.push(...(await mdFromCodeCell(cell, ++codeCellIndex, options)));
642643
break;
643644
default:
644645
throw new Error("Unexpected cell type " + cell.cell_type);
@@ -881,7 +882,7 @@ const kLangCommentChars: Record<string, string | string[]> = {
881882
dot: "//",
882883
};
883884

884-
function mdFromCodeCell(
885+
async function mdFromCodeCell(
885886
cell: JupyterCellWithOptions,
886887
cellIndex: number,
887888
options: JupyterToMarkdownOptions,
@@ -1200,14 +1201,16 @@ function mdFromCodeCell(
12001201
const caption = isCaptionableData(output)
12011202
? (outputCaptions.shift() || null)
12021203
: null;
1203-
md.push(mdOutputDisplayData(
1204-
outputLabel,
1205-
caption,
1206-
outputName + "-" + (index + 1),
1207-
output as JupyterOutputDisplayData,
1208-
options,
1209-
figureOptions,
1210-
));
1204+
md.push(
1205+
await mdOutputDisplayData(
1206+
outputLabel,
1207+
caption,
1208+
outputName + "-" + (index + 1),
1209+
output as JupyterOutputDisplayData,
1210+
options,
1211+
figureOptions,
1212+
),
1213+
);
12111214
// if this isn't an image and we have a caption, place it at the bottom of the div
12121215
if (caption && !isImage(output, options)) {
12131216
md.push(`\n${caption}\n`);
@@ -1322,7 +1325,7 @@ function mdOutputError(output: JupyterOutputError) {
13221325
return mdCodeOutput([output.ename + ": " + output.evalue]);
13231326
}
13241327

1325-
function mdOutputDisplayData(
1328+
async function mdOutputDisplayData(
13261329
label: string | null,
13271330
caption: string | null,
13281331
filename: string,
@@ -1367,7 +1370,24 @@ function mdOutputDisplayData(
13671370
lines[0] = lines[0].slice(1, -1);
13681371
return mdMarkdownOutput(lines);
13691372
} else {
1370-
return mdCodeOutput(lines.map(colors.stripColor));
1373+
if (options.toHtml) {
1374+
if (lines.some(hasAnsiEscapeCodes)) {
1375+
const html = (await Promise.all(
1376+
lines.map(convertToHtmlSpans),
1377+
));
1378+
return mdMarkdownOutput(
1379+
[
1380+
"\n::: {.ansi-escaped-output}\n```{=html}\n<pre>",
1381+
...html,
1382+
"</pre>\n```\n:::\n",
1383+
],
1384+
);
1385+
} else {
1386+
return mdCodeOutput(lines);
1387+
}
1388+
} else {
1389+
return mdCodeOutput(lines.map(colors.stripColor));
1390+
}
13711391
}
13721392
}
13731393
}

src/core/jupyter/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
kWarning,
5050
} from "../../config/constants.ts";
5151
import { FormatExecute, FormatPandoc, Metadata } from "../../config/types.ts";
52+
import { ExecuteOptions } from "../../execute/types.ts";
5253

5354
// deno-lint-ignore-file camelcase
5455

@@ -193,6 +194,7 @@ export interface JupyterOutputFigureOptions {
193194
}
194195

195196
export interface JupyterToMarkdownOptions {
197+
executeOptions: ExecuteOptions;
196198
language: string;
197199
assets: JupyterAssets;
198200
execute: FormatExecute;

src/execute/jupyter/jupyter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,10 @@ export const jupyterEngine: ExecutionEngine = {
285285
// by jupyterToMarkdown (we don't want to make a copy of a
286286
// potentially very large notebook) so should not be relied
287287
// on subseuqent to this call
288-
const result = jupyterToMarkdown(
288+
const result = await jupyterToMarkdown(
289289
nb,
290290
{
291+
executeOptions: options,
291292
language: nb.metadata.kernelspec.language.toLowerCase(),
292293
assets,
293294
execute: options.format.execute,

src/import_map.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"deno_dom/": "https://deno.land/x/[email protected]/",
3030
"media_types/": "https://deno.land/x/[email protected]/",
3131
"xmlp/": "https://deno.land/x/[email protected]/",
32+
"ansi_up": "https://cdn.skypack.dev/[email protected]",
3233
"lodash/": "https://cdn.skypack.dev/[email protected]/",
3334
"acorn/acorn": "https://cdn.skypack.dev/[email protected]",
3435
"acorn/walk": "https://cdn.skypack.dev/[email protected]",
@@ -115,4 +116,4 @@
115116
"/npm:[email protected]?dew": "./resources/vendor/dev.jspm.io/[email protected]"
116117
}
117118
}
118-
}
119+
}

src/resources/formats/html/_quarto-rules.scss

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,127 @@ div.sourceCode > iframe {
361361
border: $code-preview-border-color;
362362
}
363363
}
364+
365+
// ansi escaping
366+
div.ansi-escaped-output {
367+
font-family: monospace;
368+
display: block;
369+
}
370+
371+
/*!
372+
*
373+
* ansi colors from IPython notebook's
374+
*
375+
*/
376+
/* CSS font colors for translated ANSI escape sequences */
377+
/* The color values are a mix of
378+
http://www.xcolors.net/dl/baskerville-ivorylight and
379+
http://www.xcolors.net/dl/euphrasia */
380+
.ansi-black-fg {
381+
color: #3e424d;
382+
}
383+
.ansi-black-bg {
384+
background-color: #3e424d;
385+
}
386+
.ansi-black-intense-fg {
387+
color: #282c36;
388+
}
389+
.ansi-black-intense-bg {
390+
background-color: #282c36;
391+
}
392+
.ansi-red-fg {
393+
color: #e75c58;
394+
}
395+
.ansi-red-bg {
396+
background-color: #e75c58;
397+
}
398+
.ansi-red-intense-fg {
399+
color: #b22b31;
400+
}
401+
.ansi-red-intense-bg {
402+
background-color: #b22b31;
403+
}
404+
.ansi-green-fg {
405+
color: #00a250;
406+
}
407+
.ansi-green-bg {
408+
background-color: #00a250;
409+
}
410+
.ansi-green-intense-fg {
411+
color: #007427;
412+
}
413+
.ansi-green-intense-bg {
414+
background-color: #007427;
415+
}
416+
.ansi-yellow-fg {
417+
color: #ddb62b;
418+
}
419+
.ansi-yellow-bg {
420+
background-color: #ddb62b;
421+
}
422+
.ansi-yellow-intense-fg {
423+
color: #b27d12;
424+
}
425+
.ansi-yellow-intense-bg {
426+
background-color: #b27d12;
427+
}
428+
.ansi-blue-fg {
429+
color: #208ffb;
430+
}
431+
.ansi-blue-bg {
432+
background-color: #208ffb;
433+
}
434+
.ansi-blue-intense-fg {
435+
color: #0065ca;
436+
}
437+
.ansi-blue-intense-bg {
438+
background-color: #0065ca;
439+
}
440+
.ansi-magenta-fg {
441+
color: #d160c4;
442+
}
443+
.ansi-magenta-bg {
444+
background-color: #d160c4;
445+
}
446+
.ansi-magenta-intense-fg {
447+
color: #a03196;
448+
}
449+
.ansi-magenta-intense-bg {
450+
background-color: #a03196;
451+
}
452+
.ansi-cyan-fg {
453+
color: #60c6c8;
454+
}
455+
.ansi-cyan-bg {
456+
background-color: #60c6c8;
457+
}
458+
.ansi-cyan-intense-fg {
459+
color: #258f8f;
460+
}
461+
.ansi-cyan-intense-bg {
462+
background-color: #258f8f;
463+
}
464+
.ansi-white-fg {
465+
color: #c5c1b4;
466+
}
467+
.ansi-white-bg {
468+
background-color: #c5c1b4;
469+
}
470+
.ansi-white-intense-fg {
471+
color: #a1a6b2;
472+
}
473+
.ansi-white-intense-bg {
474+
background-color: #a1a6b2;
475+
}
476+
.ansi-default-inverse-fg {
477+
color: #ffffff;
478+
}
479+
.ansi-default-inverse-bg {
480+
background-color: #000000;
481+
}
482+
.ansi-bold {
483+
font-weight: bold;
484+
}
485+
.ansi-underline {
486+
text-decoration: underline;
487+
}

0 commit comments

Comments
 (0)