import { existsSync, walkSync } from "../deno_ral/fs.ts";
import { expandGlobSync } from "../core/deno/expand-glob.ts";
import { warning } from "../deno_ral/log.ts";
import { isSubdir } from "../deno_ral/fs.ts";
import { coerce, Range, satisfies } from "semver/mod.ts";
import {
kProjectType,
ProjectConfig,
ProjectContext,
} from "../project/types.ts";
import {
basename,
dirname,
isAbsolute,
join,
normalize,
relative,
} from "../deno_ral/path.ts";
import { Metadata, QuartoFilter } from "../config/types.ts";
import {
kSkipHidden,
normalizePath,
resolvePathGlobs,
safeExistsSync,
} from "../core/path.ts";
import { toInputRelativePaths } from "../project/project-shared.ts";
import { projectType } from "../project/types/project-types.ts";
import { mergeConfigs } from "../core/config.ts";
import { quartoConfig } from "../core/quarto.ts";
import {
kAuthor,
kBuiltInExtNames,
kBuiltInExtOrg,
kCommon,
kExtensionDir,
kQuartoRequired,
kRevealJSPlugins,
kTitle,
kVersion,
} from "./constants.ts";
import { extensionIdString } from "./extension-shared.ts";
import {
Contributes,
Extension,
ExtensionContext,
ExtensionId,
ExtensionOptions,
RevealPluginInline,
} from "./types.ts";
import { ExternalEngine } from "../resources/types/schema-types.ts";
import { cloneDeep } from "../core/lodash.ts";
import { readAndValidateYamlFromFile } from "../core/schema/validated-yaml.ts";
import { getExtensionConfigSchema } from "../core/lib/yaml-schema/project-config.ts";
import { projectIgnoreGlobs } from "../execute/engine.ts";
import { ProjectType } from "../project/types/types.ts";
import { copyResourceFile } from "../project/project-resources.ts";
import {
RevealPlugin,
RevealPluginBundle,
RevealPluginScript,
} from "../format/reveal/format-reveal-plugin-types.ts";
import { resourcePath } from "../core/resources.ts";
import { warnOnce } from "../core/log.ts";
import { existsSync1 } from "../core/file.ts";
import { kFormatResources } from "../config/constants.ts";
const kQuartoExtOrganization = "quarto-ext";
const kQuartoExtBuiltIn = ["code-filename", "grouped-tabsets"];
export function createExtensionContext(): ExtensionContext {
const extensionCache: Record<string, Extension[]> = {};
const extensions = async (
input?: string,
config?: ProjectConfig,
projectDir?: string,
options?: ExtensionOptions,
): Promise<Extension[]> => {
const extensions = await loadExtensions(
extensionCache,
input,
projectDir,
);
const results = Object.values(extensions).filter((ext) =>
options?.builtIn !== false || ext.id.organization !== kBuiltInExtOrg
);
return results.map((extension) => {
if (input) {
return resolveExtensionPaths(extension, input, config);
} else {
return extension;
}
});
};
const extension = async (
name: string,
input: string,
config?: ProjectConfig,
projectDir?: string,
): Promise<Extension | undefined> => {
const unresolved = await loadExtension(name, input, projectDir);
return resolveExtensionPaths(unresolved, input, config);
};
const find = async (
name: string,
input: string,
contributes?: Contributes,
config?: ProjectConfig,
projectDir?: string,
options?: ExtensionOptions,
): Promise<Extension[]> => {
const extId = toExtensionId(name);
return findExtensions(
await extensions(input, config, projectDir, options),
extId,
contributes,
);
};
return {
extension,
extensions,
find,
};
}
export function projectExtensionPathResolver(
libDir: string,
projectDir: string,
) {
return (href: string, projectOffset: string) => {
const projectRelativeHref = relative(projectOffset, href);
if (
projectRelativeHref.startsWith("_extensions/") ||
projectRelativeHref.startsWith("_extensions\\")
) {
const projectTargetHref = projectRelativeHref.replace(
/^_extensions/,
`${libDir}/quarto-contrib/quarto-project`,
);
copyResourceFile(
projectDir,
join(projectDir, projectRelativeHref),
join(projectDir, projectTargetHref),
);
return join(projectOffset, projectTargetHref);
}
return href;
};
}
export function filterBuiltInExtensions(
extensions: Extension[],
) {
const quartoExts = extensions.filter((ext) => {
return ext.id.organization === kBuiltInExtOrg;
});
const nowBuiltInExtensions = extensions?.filter((ext) => {
return ext.id.organization === "quarto-ext" &&
quartoExts.map((ext) => ext.id.name).includes(ext.id.name);
});
if (nowBuiltInExtensions.length > 0) {
extensions = filterExtensionAndWarn(extensions, nowBuiltInExtensions);
}
return extensions;
}
function filterExtensionAndWarn(
extensions: Extension[],
filterOutExtensions: Extension[],
) {
warnToRemoveExtensions(filterOutExtensions);
return extensions.filter((ext) => {
return !filterOutExtensions.map((ext) => ext.id.name).includes(
ext.id.name,
);
});
}
function warnToRemoveExtensions(extensions: Extension[]) {
const removeCommands = extensions.map((ext) => {
return `quarto remove extension ${extensionIdString(ext.id)}`;
});
warnOnce(
`One or more extensions have been built in to Quarto. Please use the following command to remove the unneeded extension:\n ${
removeCommands.join("\n ")
}`,
);
}
export function filterExtensions(
extensions: Extension[],
extensionId: string,
type: string,
) {
if (extensions && extensions.length > 0) {
extensions = filterBuiltInExtensions(extensions);
const ownedExtensions = extensions.filter((ext) => {
return ext.id.organization !== undefined;
}).map((ext) => {
return extensionIdString(ext.id);
});
if (ownedExtensions.length > 1) {
warnOnce(
`The ${type} '${extensionId}' matched more than one extension. Please use a full name to disambiguate:\n ${
ownedExtensions.join("\n ")
}`,
);
}
const oldBuiltInExt = extensions?.filter((ext) => {
return (ext.id.organization === kQuartoExtOrganization &&
(kQuartoExtBuiltIn.includes(ext.id.name) ||
kBuiltInExtNames.includes(ext.id.name)));
});
if (oldBuiltInExt.length > 0) {
return filterExtensionAndWarn(extensions, oldBuiltInExt);
} else {
return extensions;
}
} else {
return extensions;
}
}
export async function readSubtreeExtensions(
subtreeDir: string,
): Promise<Extension[]> {
const extensions: Extension[] = [];
const topLevelDirs = safeExistsSync(subtreeDir) &&
Deno.statSync(subtreeDir).isDirectory
? Deno.readDirSync(subtreeDir)
: [];
for (const topLevelDir of topLevelDirs) {
if (!topLevelDir.isDirectory) continue;
const dirPath = join(subtreeDir, topLevelDir.name);
const subtreeExtensionsPath = join(dirPath, kExtensionDir);
if (safeExistsSync(subtreeExtensionsPath)) {
const exts = await readExtensions(subtreeExtensionsPath);
extensions.push(...exts);
}
}
return extensions;
}
const loadExtensions = async (
cache: Record<string, Extension[]>,
input?: string,
projectDir?: string,
) => {
const extensionPath = inputExtensionDirs(input, projectDir);
const allExtensions: Record<string, Extension> = {};
const subtreePath = builtinSubtreeExtensions();
for (const extensionDir of extensionPath) {
if (cache[extensionDir]) {
cache[extensionDir].forEach((ext) => {
allExtensions[extensionIdString(ext.id)] = cloneDeep(ext);
});
} else {
const extensions = extensionDir === subtreePath
? await readSubtreeExtensions(extensionDir)
: await readExtensions(extensionDir);
extensions.forEach((extension) => {
allExtensions[extensionIdString(extension.id)] = cloneDeep(extension);
});
cache[extensionDir] = extensions;
}
}
return allExtensions;
};
const loadExtension = async (
extension: string,
input: string,
projectDir?: string,
): Promise<Extension> => {
const extensionId = toExtensionId(extension);
const extensionPath = discoverExtensionPath(input, extensionId, projectDir);
if (extensionPath) {
const file = extensionFile(extensionPath);
if (file) {
const extension = await readExtension(extensionId, file);
validateExtension(extension);
return extension;
} else {
throw new Error(
`The extension '${extension}' is missing the expected '_extension.yml' file.`,
);
}
} else {
throw new Error(
`Unable to read the extension '${extension}'.\nPlease ensure that you provided the correct id and that the extension is installed.`,
);
}
};
function findExtensions(
extensions: Extension[],
extensionId: ExtensionId,
contributes?: Contributes,
) {
const exts = extensions.filter((ext) => {
if (contributes === "shortcodes" && ext.contributes.shortcodes) {
return true;
} else if (contributes === "filters" && ext.contributes.filters) {
return true;
} else if (contributes === "formats" && ext.contributes.formats) {
return true;
} else if (contributes === "project" && ext.contributes.project) {
return true;
} else if (contributes === "metadata" && ext.contributes.metadata) {
return true;
} else if (
contributes === kRevealJSPlugins && ext.contributes[kRevealJSPlugins]
) {
return true;
} else if (contributes === "engines" && ext.contributes.engines) {
return true;
} else {
return contributes === undefined;
}
});
if (extensionId.organization) {
const exact = exts.filter((ext) => {
return (ext.id.name === extensionId.name &&
ext.id.organization === extensionId.organization);
});
if (exact.length > 0) {
return exact;
}
}
const nameMatches = exts.filter((ext) => {
return extensionId.name === ext.id.name;
});
const sortedMatches = nameMatches.sort((ext1, _ext2) => {
return ext1.id.organization === undefined ? -1 : 1;
});
return sortedMatches;
}
export function extensionProjectType(
extension: Extension,
config?: ProjectConfig,
): ProjectType {
if (extension.contributes.project) {
const projType = extension.contributes.project?.type as string || "default";
return projectType(projType);
} else {
return projectType(config?.project?.[kProjectType]);
}
}
function resolveExtensionPaths(
extension: Extension,
input: string,
config?: ProjectConfig,
) {
const inputDir = Deno.statSync(input).isDirectory ? input : dirname(input);
return toInputRelativePaths(
extensionProjectType(extension, config),
extension.path,
inputDir,
extension,
kExtensionIgnoreFields,
) as unknown as Extension;
}
const kExtensionIgnoreFields = ["biblio-style", "revealjs-plugins"];
export async function readExtensions(
extensionsDirectory: string,
organization?: string,
) {
const extensions: Extension[] = [];
const extensionDirs = safeExistsSync(extensionsDirectory) &&
Deno.statSync(extensionsDirectory).isDirectory
? Deno.readDirSync(extensionsDirectory)
: [];
for (const extensionDir of extensionDirs) {
if (extensionDir.isDirectory) {
const extFile = extensionFile(
join(extensionsDirectory, extensionDir.name),
);
if (extFile) {
const extensionId = { name: extensionDir.name, organization };
const extension = await readExtension(
extensionId,
extFile,
);
extensions.push(extension);
} else if (!organization) {
const ownedExtensions = await readExtensions(
join(extensionsDirectory, extensionDir.name),
extensionDir.name,
);
if (ownedExtensions) {
extensions.push(...ownedExtensions);
}
}
}
}
return extensions;
}
export function projectExtensionDirs(project: ProjectContext) {
const extensionDirs: string[] = [];
for (
const walk of expandGlobSync(join(project.dir, "**/_extensions"), {
exclude: [...projectIgnoreGlobs(project.dir), "**/.*", "**/.*/**"],
})
) {
extensionDirs.push(walk.path);
}
return extensionDirs;
}
export function extensionFilesFromDirs(dirs: string[]) {
const files: string[] = [];
for (const dir of dirs) {
for (
const walk of walkSync(
dir,
{
includeDirs: false,
followSymlinks: false,
skip: [kSkipHidden],
},
)
) {
files.push(walk.path);
}
}
return files;
}
export function inputExtensionDirs(input?: string, projectDir?: string) {
const extensionsDirPath = (path: string) => {
const extPath = join(path, kExtensionDir);
try {
if (Deno.statSync(extPath).isDirectory) {
return extPath;
} else {
return undefined;
}
} catch {
return undefined;
}
};
const inputDirName = (inputOrDir: string) => {
if (Deno.statSync(inputOrDir).isDirectory) {
return inputOrDir;
} else {
return dirname(inputOrDir);
}
};
const extensionDirectories: string[] = [
builtinExtensions(),
builtinSubtreeExtensions(),
];
if (projectDir && input) {
let currentDir = normalizePath(inputDirName(input));
do {
const extensionPath = extensionsDirPath(currentDir);
if (extensionPath) {
extensionDirectories.push(extensionPath);
}
currentDir = dirname(currentDir);
} while (isSubdir(projectDir, currentDir) || projectDir === currentDir);
return extensionDirectories;
} else if (input) {
const dir = extensionsDirPath(inputDirName(input));
if (dir) {
extensionDirectories.push(dir);
}
} else if (projectDir) {
const dir = extensionsDirPath(projectDir);
if (dir) {
extensionDirectories.push(dir);
}
}
return extensionDirectories;
}
export function discoverExtensionPath(
input: string,
extensionId: ExtensionId,
projectDir?: string,
) {
const extensionDirGlobs = [];
if (extensionId.organization) {
extensionDirGlobs.push(
`${extensionId.organization}/${extensionId.name}/`,
);
} else {
extensionDirGlobs.push(`${extensionId.name}/`);
extensionDirGlobs.push(`*/${extensionId.name}/`);
}
const findExtensionDir = (extDir: string, globs: string[]) => {
const paths = resolvePathGlobs(extDir, globs, [], { mode: "strict" })
.include.filter((path) => {
return extensionFile(path);
});
if (paths.length > 0) {
if (paths.length > 1) {
warning(
`More than one extension is available for ${extensionId.name} in the directory ${extDir}.\nExtensions that match:\n${
paths.join("\n")
}`,
);
}
return relative(Deno.cwd(), paths[0]);
} else {
return undefined;
}
};
const builtinExtensionDir = findExtensionDir(
builtinExtensions(),
extensionDirGlobs,
);
if (builtinExtensionDir) {
return builtinExtensionDir;
}
const subtreePath = builtinSubtreeExtensions();
if (safeExistsSync(subtreePath)) {
for (const topLevelDir of Deno.readDirSync(subtreePath)) {
if (!topLevelDir.isDirectory) continue;
const subtreeExtDir = join(subtreePath, topLevelDir.name, kExtensionDir);
if (safeExistsSync(subtreeExtDir)) {
const subtreeExtensionDir = findExtensionDir(
subtreeExtDir,
extensionDirGlobs,
);
if (subtreeExtensionDir) {
return subtreeExtensionDir;
}
}
}
}
const sourceDir = Deno.statSync(input).isDirectory ? input : dirname(input);
const sourceDirAbs = normalizePath(sourceDir);
if (projectDir && isSubdir(projectDir, sourceDirAbs)) {
let extensionDir;
let currentDir = normalize(sourceDirAbs);
const projDir = normalize(projectDir);
while (!extensionDir) {
extensionDir = findExtensionDir(
join(currentDir, kExtensionDir),
extensionDirGlobs,
);
if (currentDir == projDir) {
break;
}
currentDir = dirname(currentDir);
}
return extensionDir;
} else {
return findExtensionDir(
join(sourceDirAbs, kExtensionDir),
extensionDirGlobs,
);
}
}
function builtinExtensions() {
return resourcePath("extensions");
}
export function builtinSubtreeExtensions() {
return resourcePath("extension-subtrees");
}
function validateExtension(extension: Extension) {
let contribCount = 0;
const contribs = [
extension.contributes.filters,
extension.contributes.shortcodes,
extension.contributes.formats,
extension.contributes.project,
extension.contributes[kRevealJSPlugins],
extension.contributes.metadata,
extension.contributes.engines,
];
contribs.forEach((contrib) => {
if (contrib) {
if (Array.isArray(contrib)) {
contribCount = contribCount + contrib.length;
} else if (typeof contrib === "object") {
contribCount = contribCount + Object.keys(contrib).length;
}
}
});
if (contribCount === 0) {
throw new Error(
`The extension ${
extension.title || extension.id.name
} is not valid- it does not contribute anything.`,
);
}
if (
extension.quartoVersion &&
!satisfies(quartoConfig.version(), extension.quartoVersion)
) {
throw new Error(
`The extension ${
extension.title || extension.id.name
} is incompatible with this quarto version.
Extension requires: ${extension.quartoVersion.raw}
Quarto version: ${quartoConfig.version()}`,
);
}
}
async function readExtension(
extensionId: ExtensionId,
extensionFile: string,
): Promise<Extension> {
const extensionSchema = await getExtensionConfigSchema();
const yaml = (await readAndValidateYamlFromFile(
extensionFile,
extensionSchema,
"YAML Validation Failed",
)) as Metadata;
const readVersionRange = (str: string): Range => {
return new Range(str);
};
const contributes = yaml.contributes as Metadata | undefined;
const title = yaml[kTitle] as string;
const author = yaml[kAuthor] as string;
const versionRaw = yaml[kVersion] as string | undefined;
const quartoVersionRaw = yaml[kQuartoRequired] as string | undefined;
const versionParsed = versionRaw ? coerce(versionRaw) : undefined;
const quartoVersion = quartoVersionRaw
? readVersionRange(quartoVersionRaw)
: undefined;
const version = versionParsed ? versionParsed : undefined;
const extensionDirRaw = dirname(extensionFile);
const extensionDir = isAbsolute(extensionDirRaw)
? extensionDirRaw
: join(Deno.cwd(), extensionDirRaw);
const formats = contributes?.formats as Metadata ||
contributes?.format as Metadata || {};
const embeddedExtensions = await readExtensions(
join(extensionDir, kExtensionDir),
);
Object.keys(formats).forEach((key) => {
if (formats[key] === "default") {
formats[key] = {};
}
});
Object.keys(formats).filter((key) => {
return key !== kCommon;
}).forEach((key) => {
formats[key] = mergeConfigs(
formats[kCommon] || {},
formats[key],
);
const formatMeta = formats[key] as Metadata;
if (formatMeta[kFormatResources]) {
const resolved = resolvePathGlobs(
extensionDir,
formatMeta[kFormatResources] as string[],
[],
{ mode: "strict" },
);
if (resolved.include.length > 0) {
formatMeta[kFormatResources] = resolved.include.map((include) => {
return relative(extensionDir, include);
});
}
}
if (key.endsWith(".lua")) {
const fullPath = join(extensionDir, key);
if (existsSync(fullPath)) {
formatMeta.writer = fullPath;
}
}
formatMeta.shortcodes = (formatMeta.shortcodes as string[] || []).flatMap((
shortcode,
) => {
return resolveShortcode(embeddedExtensions, extensionDir, shortcode);
});
formatMeta.filters = (formatMeta.filters as QuartoFilter[] || []).flatMap(
(filter) => {
return resolveFilter(embeddedExtensions, extensionDir, filter);
},
);
formatMeta[kRevealJSPlugins] = (formatMeta?.[kRevealJSPlugins] as Array<
string | RevealPluginBundle | RevealPlugin
> ||
[])
.flatMap(
(plugin) => {
return resolveRevealJSPlugin(
embeddedExtensions,
extensionDir,
plugin,
);
},
);
});
delete formats[kCommon];
const shortcodes = ((contributes?.shortcodes || []) as string[]).map(
(shortcode) => {
return resolveShortcodePath(extensionDir, shortcode);
},
);
const filters = ((contributes?.filters || []) as QuartoFilter[]).map(
(filter) => {
return resolveFilterPath(extensionDir, filter);
},
);
const project = contributes?.project as Record<string, unknown> | undefined;
const metadata = contributes?.metadata as Record<string, unknown> | undefined;
for (const key of ["pre-render", "post-render", "brand"]) {
for (const object of [metadata, project]) {
if (!object?.project || typeof object.project !== "object") {
continue;
}
const t = (object.project as Record<string, unknown>)[key];
if (t) {
const value = (Array.isArray(t) ? t : [t]) as string[];
const resolved = resolvePathGlobs(
extensionDir,
value as string[],
[],
);
if (resolved.include.length > 0) {
if (key === "brand") {
let projectDir = extensionDir, last;
do {
last = basename(projectDir);
projectDir = dirname(projectDir);
} while (projectDir && last !== "_extensions");
if (projectDir) {
(object.project as Record<string, unknown>)[key] = relative(
projectDir,
resolved.include[0],
);
}
} else {
(object.project as Record<string, unknown>)[key] = resolved.include;
}
}
}
}
}
const revealJSPlugins = ((contributes?.[kRevealJSPlugins] || []) as Array<
string | RevealPluginBundle | RevealPlugin
>).map((plugin) => {
return resolveRevealPlugin(extensionDir, plugin);
});
const engines =
((contributes?.engines || []) as Array<string | ExternalEngine>).map(
(engine) => {
if (typeof engine === "string") {
return engine;
} else if (typeof engine === "object" && engine.path) {
return {
...engine,
path: join(extensionDir, engine.path),
};
}
return engine;
},
);
const result = {
title,
author,
version,
quartoVersion,
id: extensionId,
path: extensionDir,
contributes: {
metadata,
shortcodes,
filters,
formats,
project: project ?? {},
[kRevealJSPlugins]: revealJSPlugins,
engines: engines.length > 0 ? engines : undefined,
},
};
validateExtension(result);
return result;
}
function resolveRevealJSPlugin(
embeddedExtensions: Extension[],
dir: string,
plugin: string | RevealPluginBundle | RevealPlugin,
) {
if (typeof plugin === "string") {
const extensionId = toExtensionId(plugin);
const extensions = findExtensions(
embeddedExtensions,
extensionId,
"revealjs-plugins",
);
if (extensions.length > 0) {
const plugins: Array<string | RevealPluginBundle | RevealPlugin> = [];
for (const plugin of extensions[0].contributes[kRevealJSPlugins] || []) {
plugins.push(plugin);
}
return plugins;
} else {
validateExtensionPath("revealjs-plugin", dir, plugin);
return resolveRevealPlugin(dir, plugin);
}
} else {
return plugin;
}
}
export function isPluginRaw(
plugin: RevealPluginBundle | RevealPluginInline,
): plugin is RevealPluginInline {
return (plugin as RevealPluginBundle).plugin === undefined;
}
function resolveRevealPlugin(
extensionDir: string,
plugin: string | RevealPluginBundle | RevealPluginInline,
): string | RevealPluginBundle | RevealPlugin {
if (typeof plugin === "string") {
return join(extensionDir, plugin);
} else if (isPluginRaw(plugin)) {
return resolveRevealPluginInline(plugin, extensionDir);
} else {
plugin.plugin = join(extensionDir, plugin.plugin);
return plugin;
}
}
function resolveRevealPluginInline(
plugin: RevealPluginInline,
extensionDir: string,
): RevealPlugin {
if (!plugin.name) {
throw new Error(
`Invalid revealjs-plugin in ${extensionDir} - 'name' property is required.`,
);
}
const resolvedPlugin: RevealPlugin = {
name: plugin.name,
path: extensionDir,
register: plugin.register,
config: plugin.config,
};
if (plugin.script) {
const pluginArr = Array.isArray(plugin.script)
? plugin.script
: [plugin.script];
resolvedPlugin.script = pluginArr.map((plug) => {
if (typeof plug === "string") {
return {
path: plug,
} as RevealPluginScript;
} else {
return plug;
}
});
}
if (plugin.stylesheet) {
resolvedPlugin.stylesheet = Array.isArray(plugin.stylesheet)
? plugin.stylesheet
: [plugin.stylesheet];
}
return resolvedPlugin;
}
function resolveShortcode(
embeddedExtensions: Extension[],
dir: string,
shortcode: string,
) {
const extensionId = toExtensionId(shortcode);
const extensions = findExtensions(
embeddedExtensions,
extensionId,
"shortcodes",
);
if (extensions.length > 0) {
const shortcodes: string[] = [];
for (const shortcode of extensions[0].contributes.shortcodes || []) {
shortcodes.push(resolveShortcodePath(extensions[0].path, shortcode));
}
return shortcodes;
} else {
validateExtensionPath("shortcode", dir, shortcode);
return resolveShortcodePath(dir, shortcode);
}
}
function resolveShortcodePath(
extensionDir: string,
shortcode: string,
): string {
if (isAbsolute(shortcode)) {
return shortcode;
} else {
return join(extensionDir, shortcode);
}
}
function resolveFilter(
embeddedExtensions: Extension[],
dir: string,
filter: QuartoFilter,
) {
if (typeof filter === "string") {
if (filter === "quarto") {
return filter;
}
const extensionId = toExtensionId(filter);
const extensions = findExtensions(
embeddedExtensions,
extensionId,
"filters",
);
if (extensions.length > 0) {
const filters: QuartoFilter[] = [];
for (const filter of extensions[0].contributes.filters || []) {
filters.push(resolveFilterPath(extensions[0].path, filter));
}
return filters;
} else {
validateExtensionPath("filter", dir, filter);
return resolveFilterPath(dir, filter);
}
} else {
validateExtensionPath("filter", dir, filter.path);
return resolveFilterPath(dir, filter);
}
}
function resolveFilterPath(
extensionDir: string,
filter: QuartoFilter,
): QuartoFilter {
if (typeof filter === "string") {
if (isAbsolute(filter)) {
return filter;
} else {
return join(extensionDir, filter);
}
} else {
const filterAt = ((filter as any).at) as string | undefined;
const result: QuartoFilter = {
type: filter.type,
path: isAbsolute(filter.path)
? filter.path
: join(extensionDir, filter.path),
};
if (filterAt === undefined) {
return result;
} else {
return {
...result,
at: filterAt,
};
}
}
}
function validateExtensionPath(
type: "filter" | "shortcode" | "revealjs-plugin",
dir: string,
path: string,
) {
const resolves = existsSync(join(dir, path));
if (!resolves) {
throw Error(
`Failed to resolve referenced ${type} ${path} - path does not exist.\nIf you are attempting to use another extension within this extension, please install the extension using the 'quarto install --embedded' command.`,
);
}
return resolves;
}
function toExtensionId(extension: string) {
if (extension.indexOf("/") > -1) {
const extParts = extension.split("/");
if (extParts.length === 2) {
return {
name: extParts[1],
organization: extParts[0],
};
} else {
return {
name: extension,
};
}
} else {
return {
name: extension,
};
}
}
export const extensionFile = (dir: string) => {
return ["_extension.yml", "_extension.yaml"]
.map((file) => join(dir, file))
.find(existsSync1);
};