Skip to content

Commit 062d393

Browse files
authored
Merge branch 'main' into when-format-more
2 parents 77cec07 + f261541 commit 062d393

File tree

101 files changed

+3007
-1717
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+3007
-1717
lines changed

news/changelog-1.7.md

Lines changed: 76 additions & 60 deletions
Large diffs are not rendered by default.

src/command/add/cmd.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createTempContext } from "../../core/temp.ts";
99
import { installExtension } from "../../extension/install.ts";
1010

1111
import { info } from "../../deno_ral/log.ts";
12+
import { signalCommandFailure } from "../utils.ts";
1213

1314
export const addCommand = new Command()
1415
.name("add")
@@ -44,16 +45,19 @@ export const addCommand = new Command()
4445
await initYamlIntelligenceResourcesFromFilesystem();
4546
const temp = createTempContext();
4647
try {
47-
// Install an extension
48-
if (extension) {
49-
await installExtension(
50-
extension,
51-
temp,
52-
options.prompt !== false,
53-
options.embed,
54-
);
55-
} else {
48+
if (!extension) {
5649
info("Please provide an extension name, url, or path.");
50+
signalCommandFailure();
51+
}
52+
// Install an extension
53+
const result = await installExtension(
54+
extension,
55+
temp,
56+
options.prompt !== false,
57+
options.embed,
58+
);
59+
if (!result) {
60+
signalCommandFailure();
5761
}
5862
} finally {
5963
temp.cleanup();

src/command/convert/jupyter.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,6 @@ export async function jupyterNotebookToMarkdown(
132132
}
133133
}
134134

135-
console.log({ md });
136-
137135
// join into source
138136
const mdSource = md.join("");
139137

src/command/list/cmd.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { outputTools } from "../../tools/tools-console.ts";
1717
import { notebookContext } from "../../render/notebook/notebook-context.ts";
1818

1919
export const listCommand = new Command()
20-
.hidden()
2120
.name("list")
2221
.arguments("<type:string>")
2322
.description(

src/command/remove/cmd.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
selectTool,
2323
} from "../../tools/tools-console.ts";
2424
import { notebookContext } from "../../render/notebook/notebook-context.ts";
25+
import { signalCommandFailure } from "../utils.ts";
2526

2627
export const removeCommand = new Command()
2728
.name("remove")
@@ -69,6 +70,7 @@ export const removeCommand = new Command()
6970
const allTools = await loadTools();
7071
if (allTools.filter((tool) => tool.installed).length === 0) {
7172
info("No tools are installed.");
73+
signalCommandFailure();
7274
} else {
7375
// Select which tool should be installed
7476
const toolTarget = await selectTool(allTools, "remove");
@@ -118,6 +120,7 @@ export const removeCommand = new Command()
118120
await removeExtensions(extensions.slice(), options.prompt);
119121
} else {
120122
info("No matching extension found.");
123+
signalCommandFailure();
121124
}
122125
} else {
123126
const nbContext = notebookContext();
@@ -138,6 +141,7 @@ export const removeCommand = new Command()
138141
}
139142
} else {
140143
info("No extensions installed.");
144+
signalCommandFailure();
141145
}
142146
}
143147
}

src/command/render/pandoc-html.ts

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export async function resolveSassBundles(
142142
)
143143
? "dark"
144144
: "light";
145-
const targets: SassTarget[] = [{
145+
let targets: SassTarget[] = [{
146146
name: `${dependency}.min.css`,
147147
bundles: (bundles as any),
148148
attribs: {
@@ -175,10 +175,22 @@ export async function resolveSassBundles(
175175
...attribForThemeStyle("dark"),
176176
},
177177
};
178-
if (defaultStyle === "dark") {
178+
if (defaultStyle === "dark") { // light, dark
179179
targets.push(darkTarget);
180-
} else {
181-
targets.unshift(darkTarget);
180+
} else { // light, dark, light
181+
const lightTargetExtra = {
182+
...targets[0],
183+
attribs: {
184+
...targets[0].attribs,
185+
class: "quarto-color-scheme-extra",
186+
},
187+
};
188+
189+
targets = [
190+
targets[0],
191+
darkTarget,
192+
lightTargetExtra,
193+
];
182194
}
183195

184196
hasDarkStyles = true;
@@ -498,34 +510,67 @@ function generateThemeCssClasses(
498510
Record<string, unknown>
499511
>;
500512
if (textStyles) {
501-
const lines: string[] = [];
513+
const otherLines: string[] = [];
514+
otherLines.push("/* syntax highlight based on Pandoc's rules */");
515+
const tokenCssByAbbr: Record<string, string[]> = {};
516+
517+
const toCSS = function (
518+
abbr: string,
519+
styleName: string,
520+
cssValues: string[],
521+
) {
522+
const lines: string[] = [];
523+
lines.push(`/* ${styleName} */`);
524+
lines.push(`\ncode span${abbr !== "" ? `.${abbr}` : ""} {`);
525+
cssValues.forEach((value) => {
526+
lines.push(` ${value}`);
527+
});
528+
lines.push("}\n");
529+
530+
// Store by abbreviation for sorting later
531+
tokenCssByAbbr[abbr] = lines;
532+
};
502533

503534
Object.keys(textStyles).forEach((styleName) => {
504535
const abbr = kAbbrevs[styleName];
505536
if (abbr !== undefined) {
506537
const textValues = textStyles[styleName];
507538
const cssValues = generateCssKeyValues(textValues);
508539

509-
if (abbr !== "") {
510-
lines.push(`\ncode span.${abbr} {`);
511-
lines.push(...cssValues);
512-
lines.push("}\n");
513-
} else {
540+
toCSS(abbr, styleName, cssValues);
541+
542+
if (abbr == "") {
514543
[
515544
"pre > code.sourceCode > span",
516-
"code span",
517545
"code.sourceCode > span",
518546
"div.sourceCode,\ndiv.sourceCode pre.sourceCode",
519547
]
520548
.forEach((selector) => {
521-
lines.push(`\n${selector} {`);
522-
lines.push(...cssValues);
523-
lines.push("}\n");
549+
otherLines.push(`\n${selector} {`);
550+
otherLines.push(...cssValues);
551+
otherLines.push("}\n");
524552
});
525553
}
526554
}
527555
});
528-
return lines;
556+
557+
// Sort tokenCssLines by abbr and flatten them
558+
// Ensure empty abbr ("") comes first by using a custom sort function
559+
const sortedTokenCssLines: string[] = [];
560+
Object.keys(tokenCssByAbbr)
561+
.sort((a, b) => {
562+
// Empty string ("") should come first
563+
if (a === "") return -1;
564+
if (b === "") return 1;
565+
// Otherwise normal alphabetical sort
566+
return a.localeCompare(b);
567+
})
568+
.forEach((abbr) => {
569+
sortedTokenCssLines.push(...tokenCssByAbbr[abbr]);
570+
});
571+
572+
// return otherLines followed by tokenCssLines (now sorted by abbr)
573+
return otherLines.concat(sortedTokenCssLines);
529574
}
530575
return undefined;
531576
}

src/command/render/template.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -270,26 +270,47 @@ function katexScript(url: string) {
270270
link.rel = "stylesheet";
271271
link.href = "${url}katex.min.css";
272272
head.appendChild(link);
273-
274-
var script = document.createElement("script");
275-
script.type = "text/javascript";
276-
script.src = "${url}katex.min.js";
277-
script.async = false;
278-
script.addEventListener('load', function() {
273+
274+
function renderMathElements() {
279275
var mathElements = document.getElementsByClassName("math");
280-
var macros = [];
281-
for (var i = 0; i < mathElements.length; i++) {
282-
var texText = mathElements[i].firstChild;
283-
if (mathElements[i].tagName == "SPAN") {
276+
var macros = [];
277+
for (var i = 0; i < mathElements.length; i++) {
278+
var texText = mathElements[i].firstChild;
279+
if (mathElements[i].tagName == "SPAN") {
280+
if (window.katex) {
284281
window.katex.render(texText.data, mathElements[i], {
285282
displayMode: mathElements[i].classList.contains('display'),
286283
throwOnError: false,
287284
macros: macros,
288285
fleqn: false
289286
});
287+
} else {
288+
console.error("KaTeX has not been loaded correctly, as not found globally.");
290289
}
291290
}
292-
});
291+
}
292+
}
293+
294+
var script = document.createElement("script");
295+
script.src = "${url}katex.min.js";
296+
script.onload = renderMathElements;
297+
298+
// Check for RequireJS and AMD detection as it conflicts with KaTeX loading.
299+
if (typeof require === 'function' && typeof define === 'function' && define.amd) {
300+
// Disable require.js AMD detection temporarily, as it conflicts with KaTeX loading using CommonJS
301+
var disableAmdScript = document.createElement("script");
302+
disableAmdScript.textContent = 'window._amd_backup = window.define.amd; window.define.amd = false;';
303+
head.appendChild(disableAmdScript);
304+
305+
// overwrite onload to restore Require.js AMD detection
306+
script.onload = function() {
307+
// Restore Require.js AMD detection
308+
window.define.amd = window._amd_backup;
309+
delete window._amd_backup;
310+
renderMathElements();
311+
};
312+
}
313+
293314
head.appendChild(script);
294315
});
295316
</script>

src/command/utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* utils.ts
3+
*
4+
* Copyright (C) 2025 Posit Software, PBC
5+
*/
6+
7+
let someCommandFailed = false;
8+
9+
// we do this the roundabout way because there doesn't seem to be any clean way
10+
// for cliffy commands to return values? Likely a skill issue on my part
11+
export const signalCommandFailure = () => {
12+
someCommandFailed = true;
13+
};
14+
15+
export const commandFailed = () => {
16+
return someCommandFailed;
17+
};

src/core/jupyter/jupyter.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ import {
174174
jupyterCellSrcAsLines,
175175
jupyterCellSrcAsStr,
176176
} from "./jupyter-shared.ts";
177+
import { error } from "../../deno_ral/log.ts";
178+
import { valid } from "semver/mod.ts";
177179

178180
export const kQuartoMimeType = "quarto_mimetype";
179181
export const kQuartoOutputOrder = "quarto_order";
@@ -921,8 +923,44 @@ export function jupyterCellWithOptions(
921923
}
922924
};
923925

926+
const validMetadata: Record<
927+
string,
928+
string | number | boolean | null | Array<unknown>
929+
> = {};
930+
for (const key of Object.keys(cell.metadata)) {
931+
const value = cell.metadata[key];
932+
let jsonEncodedKeyIndex = 0;
933+
if (value !== undefined) {
934+
if (!value && typeof value === "object") {
935+
validMetadata[key] = null;
936+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
937+
// https://github.com/quarto-dev/quarto-cli/issues/9089
938+
// we need to json-encode this and signal the encoding in the key
939+
// we can't use the key as is since it may contain invalid characters
940+
// and modifying the key might introduce collisions
941+
// we ensure the key is unique with a counter, and assume
942+
// "quarto-private-*" to be a private namespace for quarto.
943+
// we'd prefer to use _quarto-* instead, but Pandoc doesn't allow keys to start
944+
// with an underscore.
945+
validMetadata[
946+
`quarto-private-${++jsonEncodedKeyIndex}`
947+
] = JSON.stringify({ key, value });
948+
} else if (
949+
typeof value === "string" || typeof value === "number" ||
950+
typeof value === "boolean" || Array.isArray(value)
951+
) {
952+
validMetadata[key] = value;
953+
} else {
954+
error(
955+
`Invalid metadata type for key ${key}: ${typeof value}. Entry will not be serialized.`,
956+
);
957+
}
958+
}
959+
}
960+
924961
return {
925962
...cell,
963+
metadata: validMetadata,
926964
id: cellId(cell),
927965
source,
928966
optionsSource,
@@ -1766,7 +1804,10 @@ function isMarkdown(output: JupyterOutput, options: JupyterToMarkdownOptions) {
17661804
return isDisplayDataType(output, options, displayDataIsMarkdown);
17671805
}
17681806

1769-
async function mdOutputStream(output: JupyterOutputStream, options: JupyterToMarkdownOptions) {
1807+
async function mdOutputStream(
1808+
output: JupyterOutputStream,
1809+
options: JupyterToMarkdownOptions,
1810+
) {
17701811
let text: string[] = [];
17711812
if (typeof output.text === "string") {
17721813
text = [output.text];
@@ -1873,8 +1914,11 @@ async function mdOutputDisplayData(
18731914
// if output is invalid, warn and emit empty
18741915
const data = output.data[mimeType] as unknown;
18751916
if (!Array.isArray(data) || data.some((s) => typeof s !== "string")) {
1876-
return await mdWarningOutput(`Unable to process text plain output data
1877-
which does not appear to be plain text: ${JSON.stringify(data)}`, options);
1917+
return await mdWarningOutput(
1918+
`Unable to process text plain output data
1919+
which does not appear to be plain text: ${JSON.stringify(data)}`,
1920+
options,
1921+
);
18781922
}
18791923
const lines = data as string[];
18801924
// pandas inexplicably outputs html tables as text/plain with an enclosing single-quote
@@ -1911,7 +1955,7 @@ which does not appear to be plain text: ${JSON.stringify(data)}`, options);
19111955
// no type match found
19121956
return await mdWarningOutput(
19131957
"Unable to display output for mime type(s): " +
1914-
Object.keys(output.data).join(", "),
1958+
Object.keys(output.data).join(", "),
19151959
options,
19161960
);
19171961
}

src/core/pandoc/self-contained.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,15 @@ export const pandocIngestSelfContainedContent = async (
4848

4949
// The raw html contents
5050
const contents = Deno.readTextFileSync(file);
51+
const doctypeMatch = contents.match(/^<!DOCTYPE.*?>/);
5152
const dom = await parseHtml(contents);
5253
await bundleModules(dom, workingDir);
5354

5455
const input: string[] = [];
5556
input.push("````````{=html}");
57+
if (doctypeMatch) {
58+
input.push(doctypeMatch[0]);
59+
}
5660
input.push(dom.documentElement!.outerHTML);
5761
input.push("````````");
5862

0 commit comments

Comments
 (0)