Skip to content
Draft
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 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
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
24 changes: 17 additions & 7 deletions src/core/pandoc/pandoc-partition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,29 @@ 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-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());
}
49 changes: 0 additions & 49 deletions src/execute/engine-shared.ts

This file was deleted.

28 changes: 21 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 "./engine-shared.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,31 @@ 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 = 0;

// 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);
// Convert boolean to number for backwards compatibility
const score = typeof claim === "boolean" ? (claim ? 1 : 0) : 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
2 changes: 1 addition & 1 deletion src/execute/ojs/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import { logError } from "../../core/log.ts";
import { breakQuartoMd, QuartoMdCell } from "../../core/lib/break-quarto-md.ts";

import { MappedString } from "../../core/mapped-text.ts";
import { languagesInMarkdown } from "../engine-shared.ts";
import { languagesInMarkdown } from "../../core/pandoc/pandoc-partition.ts";

import {
pandocBlock,
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 boolean for simple claim, or number for priority (higher wins, 0 = no claim, 1 = standard claim)
*/
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 a number > 1 to override other engines that also claim this language.
return language.toLowerCase() === kCellLanguage.toLowerCase();
},

Expand Down
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Minimal engine to test class-based engine override
// Claims {python.foo} blocks with priority 2 (higher than Jupyter's 1)

let quarto;

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

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

claimsFile: (_file, _ext) => false,

claimsLanguage: (language, firstClass) => {
// Claim python.foo with priority 2 (overrides Jupyter's 1)
if (language === "python" && firstClass === "foo") {
return 2;
}
return 0;
},

canFreeze: false,
generatesFigures: false,

launch: (context) => {
return {
name: "foo",
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(`::: {#foo-engine-marker .foo-engine-output}
**FOO 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 fooEngineDiscovery;
Loading
Loading