Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion packages/quarto-types/dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,13 @@ export interface QuartoAPI {
* @returns Set of language identifiers found in fenced code blocks
*/
getLanguages: (markdown: string) => Set<string>;
/**
* Extract programming languages and their first class from code blocks
*
* @param markdown - Markdown content to analyze
* @returns Map of language identifiers to their first class (or undefined)
*/
getLanguagesWithClasses: (markdown: string) => Map<string, string | undefined>;
/**
* Break Quarto markdown into cells
*
Expand Down Expand Up @@ -1566,8 +1573,12 @@ export interface ExecutionEngineDiscovery {
claimsFile: (file: string, ext: string) => boolean;
/**
* Whether this engine can handle the given language
*
* @param language - The language identifier (e.g., "python", "r", "julia")
* @param firstClass - Optional first class from code block attributes (e.g., "marimo" from {python .marimo})
* @returns boolean for simple claim, or number for priority (higher wins, 0 = no claim, 1 = standard claim)
*/
claimsLanguage: (language: string) => boolean;
claimsLanguage: (language: string, firstClass?: string) => boolean | number;
/**
* Whether this engine supports freezing
*/
Expand Down
6 changes: 5 additions & 1 deletion packages/quarto-types/src/execution-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,12 @@ export interface ExecutionEngineDiscovery {

/**
* Whether this engine can handle the given language
*
* @param language - The language identifier (e.g., "python", "r", "julia")
* @param firstClass - Optional first class from code block attributes (e.g., "marimo" from {python .marimo})
* @returns false to skip (don't claim), true to claim with priority 1, or any number for custom priority (higher wins)
*/
claimsLanguage: (language: string) => boolean;
claimsLanguage: (language: string, firstClass?: string) => boolean | number;

/**
* Whether this engine supports freezing
Expand Down
8 changes: 8 additions & 0 deletions packages/quarto-types/src/quarto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ export interface QuartoAPI {
*/
getLanguages: (markdown: string) => Set<string>;

/**
* Extract programming languages and their first class from code blocks
*
* @param markdown - Markdown content to analyze
* @returns Map of language identifiers to their first class (or undefined)
*/
getLanguagesWithClasses: (markdown: string) => Map<string, string | undefined>;

/**
* Break Quarto markdown into cells
*
Expand Down
2 changes: 2 additions & 0 deletions src/core/api/markdown-regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { MarkdownRegexNamespace } from "./types.ts";
import { readYamlFromMarkdown } from "../yaml.ts";
import {
languagesInMarkdown,
languagesWithClasses,
partitionMarkdown,
} from "../pandoc/pandoc-partition.ts";
import { breakQuartoMd } from "../lib/break-quarto-md.ts";
Expand All @@ -17,6 +18,7 @@ globalRegistry.register("markdownRegex", (): MarkdownRegexNamespace => {
extractYaml: readYamlFromMarkdown,
partition: partitionMarkdown,
getLanguages: languagesInMarkdown,
getLanguagesWithClasses: languagesWithClasses,
breakQuartoMd,
};
});
3 changes: 3 additions & 0 deletions src/core/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export interface MarkdownRegexNamespace {
extractYaml: (markdown: string) => Metadata;
partition: (markdown: string) => PartitionedMarkdown;
getLanguages: (markdown: string) => Set<string>;
getLanguagesWithClasses: (
markdown: string,
) => Map<string, string | undefined>;
breakQuartoMd: (
src: string | MappedString,
validate?: boolean,
Expand Down
2 changes: 1 addition & 1 deletion src/core/lib/break-quarto-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function breakQuartoMd(
// regexes
const yamlRegEx = /^---\s*$/;
const startCodeCellRegEx = startCodeCellRegex || new RegExp(
"^\\s*(```+)\\s*\\{([=A-Za-z]+)( *[ ,].*)?\\}\\s*$",
"^\\s*(```+)\\s*\\{([=A-Za-z][=A-Za-z0-9._]*)( *[ ,].*)?\\}\\s*$",
);
const startCodeRegEx = /^```/;
const endCodeRegEx = /^\s*(```+)\s*$/;
Expand Down
25 changes: 18 additions & 7 deletions src/core/pandoc/pandoc-partition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,30 @@ export function languagesInMarkdownFile(file: string) {
return languagesInMarkdown(Deno.readTextFileSync(file));
}

export function languagesInMarkdown(markdown: string) {
// see if there are any code chunks in the file
const languages = new Set<string>();
const kChunkRegex = /^[\t >]*```+\s*\{([a-zA-Z0-9_]+)( *[ ,].*)?\}\s*$/gm;
export function languagesWithClasses(
markdown: string,
): Map<string, string | undefined> {
const result = new Map<string, string | undefined>();
// Capture language and everything after it (including dot-joined classes like {python.marimo})
const kChunkRegex =
/^[\t >]*```+\s*\{([a-zA-Z][a-zA-Z0-9_.]*)([^}]*)?\}\s*$/gm;
kChunkRegex.lastIndex = 0;
let match = kChunkRegex.exec(markdown);
while (match) {
const language = match[1].toLowerCase();
if (!languages.has(language)) {
languages.add(language);
if (!result.has(language)) {
// Extract first class from attrs (group 2)
// Handles {python.marimo}, {python .marimo}, {python #id .marimo}, etc.
const attrs = match[2];
const firstClass = attrs?.match(/\.([a-zA-Z][a-zA-Z0-9_-]*)/)?.[1];
result.set(language, firstClass);
}
match = kChunkRegex.exec(markdown);
}
kChunkRegex.lastIndex = 0;
return languages;
return result;
}

export function languagesInMarkdown(markdown: string): Set<string> {
return new Set(languagesWithClasses(markdown).keys());
}
32 changes: 25 additions & 7 deletions src/execute/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import {
ExecutionTarget,
kQmdExtensions,
} from "./types.ts";
import { languagesInMarkdown } from "../core/pandoc/pandoc-partition.ts";
import {
languagesInMarkdown,
languagesWithClasses,
} from "../core/pandoc/pandoc-partition.ts";
import { languages as handlerLanguages } from "../core/handlers/base.ts";
import { RenderContext, RenderFlags } from "../command/render/types.ts";
import { mergeConfigs } from "../core/config.ts";
Expand Down Expand Up @@ -168,20 +171,35 @@ export function markdownExecutionEngine(
}

// if there are languages see if any engines want to claim them
const languages = languagesInMarkdown(markdown);
const languagesWithClassesMap = languagesWithClasses(markdown);

// see if there is an engine that claims this language (highest score wins)
for (const [language, firstClass] of languagesWithClassesMap) {
let bestEngine: ExecutionEngineDiscovery | undefined;
let bestScore = -Infinity;

// see if there is an engine that claims this language
for (const language of languages) {
for (const [_, engine] of reorderedEngines) {
if (engine.claimsLanguage(language)) {
return engine.launch(engineProjectContext(project));
const claim = engine.claimsLanguage(language, firstClass);
// false means "don't claim", skip this engine entirely
if (claim === false) {
continue;
}
// true -> score 1, number -> use as score
const score = claim === true ? 1 : claim;
if (score > bestScore) {
bestScore = score;
bestEngine = engine;
}
}

if (bestEngine) {
return bestEngine.launch(engineProjectContext(project));
}
}

const handlerLanguagesVal = handlerLanguages();
// if there is a non-cell handler language then this must be jupyter
for (const language of languages) {
for (const language of languagesWithClassesMap.keys()) {
if (language !== "ojs" && !handlerLanguagesVal.includes(language)) {
return jupyterEngineDiscovery.launch(engineProjectContext(project));
}
Expand Down
9 changes: 8 additions & 1 deletion src/execute/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,14 @@ export interface ExecutionEngineDiscovery {
defaultContent: (kernel?: string) => string[];
validExtensions: () => string[];
claimsFile: (file: string, ext: string) => boolean;
claimsLanguage: (language: string) => boolean;
/**
* Whether this engine can handle the given language
*
* @param language - The language identifier (e.g., "python", "r", "julia")
* @param firstClass - Optional first class from code block attributes (e.g., "marimo" from {python .marimo})
* @returns false to skip (don't claim), true to claim with priority 1, or any number for custom priority (higher wins)
*/
claimsLanguage: (language: string, firstClass?: string) => boolean | number;
canFreeze: boolean;
generatesFigures: boolean;
ignoreDirs?: () => string[] | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,14 @@ const exampleEngineDiscovery: ExecutionEngineDiscovery = {
return false;
},

claimsLanguage: (language: string) => {
// This engine claims cells with its own language name
claimsLanguage: (
language: string,
_firstClass?: string,
): boolean | number => {
// This engine claims cells with its own language name.
// The optional firstClass parameter allows claiming based on code block class
// (e.g., {python .myengine} would have firstClass="myengine").
// Return false to skip, true to claim with priority 1, or any number for custom priority.
return language.toLowerCase() === kCellLanguage.toLowerCase();
},

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
title: Bar Engine
author: Quarto Dev Team
version: 1.0.0
quarto-required: ">=1.9.17"
contributes:
engines:
- path: bar-engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Engine to test priority-based engine override
// Claims {python.foo} blocks with priority 3 (higher than foo-engine's 2)

let quarto;

const barEngineDiscovery = {
init: (quartoAPI) => {
quarto = quartoAPI;
},

name: "bar",
defaultExt: ".qmd",
defaultYaml: () => ["engine: bar"],
defaultContent: () => ["# Bar Engine Document"],
validExtensions: () => [".qmd"],

claimsFile: (_file, _ext) => false,

claimsLanguage: (language, firstClass) => {
// Claim python.foo with priority 3 (overrides foo-engine's 2)
if (language === "python" && firstClass === "foo") {
return 3;
}
return false; // Don't claim
},

canFreeze: false,
generatesFigures: false,

launch: (context) => {
return {
name: "bar",
canFreeze: false,

markdownForFile: async (file) => {
return quarto.mappedString.fromFile(file);
},

target: async (file, _quiet, markdown) => {
if (!markdown) {
markdown = quarto.mappedString.fromFile(file);
}
const metadata = quarto.markdownRegex.extractYaml(markdown.value);
return {
source: file,
input: file,
markdown,
metadata,
};
},

partitionedMarkdown: async (file) => {
const markdown = quarto.mappedString.fromFile(file);
return quarto.markdownRegex.partition(markdown.value);
},

execute: async (options) => {
const chunks = await quarto.markdownRegex.breakQuartoMd(
options.target.markdown,
);

const processedCells = [];
for (const cell of chunks.cells) {
if (
typeof cell.cell_type === "object" &&
cell.cell_type.language === "python"
) {
const header = cell.sourceVerbatim.value.split(/\r?\n/)[0];
const hasClassFoo = /\.foo\b/.test(header);
if (hasClassFoo) {
processedCells.push(`::: {#bar-engine-marker .bar-engine-output}
**BAR ENGINE PROCESSED THIS BLOCK**

Original code:
\`\`\`python
${cell.source.value.trim()}
\`\`\`
:::
`);
continue;
}
}
processedCells.push(cell.sourceVerbatim.value);
}

return {
markdown: processedCells.join(""),
supporting: [],
filters: [],
};
},

dependencies: async (_options) => {
return { includes: {} };
},

postprocess: async (_options) => {},
};
},
};

export default barEngineDiscovery;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
title: Foo Engine
author: Quarto Dev Team
version: 1.0.0
quarto-required: ">=1.9.17"
contributes:
engines:
- path: foo-engine.js
Loading
Loading