diff --git a/src/command/preview/preview.ts b/src/command/preview/preview.ts index 6c8bdab0e32..f07df8a39f2 100644 --- a/src/command/preview/preview.ts +++ b/src/command/preview/preview.ts @@ -57,7 +57,11 @@ import { renderFormats } from "../render/render-contexts.ts"; import { renderResultFinalOutput } from "../render/render.ts"; import { replacePandocArg } from "../render/flags.ts"; -import { Format, isPandocFilter } from "../../config/types.ts"; +import { + Format, + isPandocFilter, + isQuartoFilterEntryPointQualifiedFull, +} from "../../config/types.ts"; import { kPdfJsInitialPath, pdfJsBaseDir, @@ -467,17 +471,34 @@ export async function renderForPreview( extensionFiles.push(...renderResult.files.reduce( (extensionFiles: string[], file: RenderResultFile) => { const shortcodes = file.format.render.shortcodes || []; - const filters = (file.format.pandoc.filters || []).map((filter) => - isPandocFilter(filter) ? filter.path : filter - ); + const filters = (file.format.pandoc.filters || []).map((filter) => { + if (isPandocFilter(filter)) { + return filter.path; + } + if (isQuartoFilterEntryPointQualifiedFull(filter)) { + switch (filter.path.type) { + case "absolute": + return filter.path.path; + case "relative": + return join(dirname(file.input), filter.path.path); + case "project-relative": + return join( + project?.dir ?? dirname(file.input), + filter.path.path, + ); + } + } + return filter; + }); const ipynbFilters = file.format.execute["ipynb-filters"] || []; [...shortcodes, ...filters.map((filter) => filter), ...ipynbFilters] .forEach((extensionFile) => { if (!isAbsolute(extensionFile)) { - const extensionFullPath = join(dirname(file.input), extensionFile); - if (existsSync(extensionFullPath)) { - extensionFiles.push(normalizePath(extensionFullPath)); - } + extensionFile = join(dirname(file.input), extensionFile); + } + // const extensionFullPath = join(dirname(file.input), extensionFile); + if (existsSync(extensionFile)) { + extensionFiles.push(normalizePath(extensionFile)); } }); return extensionFiles; diff --git a/src/command/render/filters.ts b/src/command/render/filters.ts index 1081b5fc6ec..33ae6bc0e8c 100644 --- a/src/command/render/filters.ts +++ b/src/command/render/filters.ts @@ -59,12 +59,13 @@ import { PandocOptions } from "./types.ts"; import { Format, FormatPandoc, - isFilterEntryPoint, QuartoFilter, QuartoFilterEntryPoint, + QuartoFilterEntryPointQualified, + QuartoFilterEntryPointQualifiedFull, } from "../../config/types.ts"; import { QuartoFilterSpec } from "./types.ts"; -import { Metadata } from "../../config/types.ts"; +import { Metadata, QualifiedPath } from "../../config/types.ts"; import { kProjectType } from "../../project/types.ts"; import { bibEngine } from "../../config/pdf.ts"; import { rBinaryPath, resourcePath } from "../../core/resources.ts"; @@ -85,7 +86,13 @@ import { quartoConfig } from "../../core/quarto.ts"; import { metadataNormalizationFilterActive } from "./normalize.ts"; import { kCodeAnnotations } from "../../format/html/format-html-shared.ts"; import { projectOutputDir } from "../../project/project-shared.ts"; -import { relative } from "../../deno_ral/path.ts"; +import { + dirname, + extname, + join, + relative, + resolve, +} from "../../deno_ral/path.ts"; import { citeIndexFilterParams } from "../../project/project-cites.ts"; import { debug } from "../../deno_ral/log.ts"; import { kJatsSubarticle } from "../../format/jats/format-jats-types.ts"; @@ -95,6 +102,7 @@ import { pythonExec } from "../../core/jupyter/exec.ts"; import { kTocIndent } from "../../config/constants.ts"; import { isWindows } from "../../deno_ral/platform.ts"; import { tinyTexBinDir } from "../../tools/impl/tinytex-info.ts"; +import { warn } from "log"; const kQuartoParams = "quarto-params"; @@ -599,7 +607,13 @@ async function quartoFilterParams( } const shortcodes = format.render[kShortcodes]; if (shortcodes !== undefined) { - params[kShortcodes] = shortcodes; + params[kShortcodes] = shortcodes.map((p) => { + if (p.startsWith("/")) { + return resolve(join(options.project.dir, p)); + } else { + return p; + } + }); } const extShortcodes = await extensionShortcodes(options); if (extShortcodes) { @@ -716,7 +730,7 @@ const kQuartoCiteProcMarker = "citeproc"; // NB: this mutates `pandoc.citeproc` export async function resolveFilters( - filters: QuartoFilter[], + filtersParam: QuartoFilter[], options: PandocOptions, pandoc: FormatPandoc, ): Promise { @@ -729,8 +743,10 @@ export async function resolveFilters( quartoFilters.push(quartoMainFilter()); // Resolve any filters that are provided by an extension - filters = await resolveFilterExtension(options, filters); - let quartoLoc = filters.findIndex((filter) => filter === kQuartoFilterMarker); + const filters = await resolveFilterExtension(options, filtersParam); + let quartoLoc = filters.findIndex((filter) => + filter.type === kQuartoFilterMarker + ); if (quartoLoc === -1) { quartoLoc = Infinity; // if no quarto marker, put our filters at the beginning } @@ -742,28 +758,39 @@ export async function resolveFilters( // if 'quarto' is not in the filter, all declarations go to the kQuartoPre entry point // // (note that citeproc will in all cases run last) - const entryPoints: QuartoFilterEntryPoint[] = filters - .filter((f) => f !== "quarto") // remove quarto marker + + // citeproc at the very end so all other filters can interact with citations + + // remove special filter markers + const fullFilters = filters.filter((filter) => + filter.type !== kQuartoCiteProcMarker && filter.type !== kQuartoFilterMarker + ) as QuartoFilterEntryPointQualifiedFull[]; + + const resolvePath = (filter: QuartoFilterEntryPointQualifiedFull["path"]) => { + switch (filter.type) { + case "absolute": + return filter.path; + case "relative": + return resolve(dirname(options.source), filter.path); + case "project-relative": + return resolve(join(options.project.dir, filter.path)); + } + }; + + const entryPoints: QuartoFilterEntryPoint[] = fullFilters .map((filter, i) => { - if (isFilterEntryPoint(filter)) { - return filter; // send entry-point-style filters unchanged - } - const at = quartoLoc > i ? kQuartoPre : kQuartoPost; - const result: QuartoFilterEntryPoint = typeof filter === "string" - ? { - "at": at, - "type": filter.endsWith(".lua") ? "lua" : "json", - "path": filter, - } - : { - "at": at, - ...filter, - }; + const at = filter.at === "__quarto-auto" + ? (quartoLoc > i ? kQuartoPre : kQuartoPost) + : filter.at; + + const result: QuartoFilterEntryPoint = { + "at": at, + "type": filter.type, + "path": resolvePath(filter.path), + }; return result; }); - // citeproc at the very end so all other filters can interact with citations - filters = filters.filter((filter) => filter !== kQuartoCiteProcMarker); const citeproc = citeMethod(options) === kQuartoCiteProcMarker; if (citeproc) { // If we're explicitely adding the citeproc filter, turn off @@ -844,60 +871,158 @@ function pdfEngine(options: PandocOptions): string { return pdfEngine; } +// Resolve any filters that are provided by an extension async function resolveFilterExtension( options: PandocOptions, filters: QuartoFilter[], -): Promise { - // Resolve any filters that are provided by an extension - const results: (QuartoFilter | QuartoFilter[])[] = []; - const getFilter = async (filter: QuartoFilter) => { - // Look for extension names in the filter list and result them - // into the filters provided by the extension - if ( - filter !== kQuartoFilterMarker && filter !== kQuartoCiteProcMarker && - typeof filter === "string" - ) { - // The filter string points to an executable file which exists - if (existsSync(filter) && !Deno.statSync(filter).isDirectory) { - return filter; - } - - const extensions = await options.services.extension?.find( - filter, - options.source, - "filters", - options.project?.config, - options.project?.dir, - ) || []; - - // Filter this list of extensions - const filteredExtensions = filterExtensions( - extensions || [], - filter, - "filter", - ); - // Return any contributed plugins - if (filteredExtensions.length > 0) { - // This matches an extension, use the contributed filters - const filters = extensions[0].contributes.filters; - if (filters) { - return filters; - } else { - return filter; +): Promise { + const results: + (QuartoFilterEntryPointQualified | QuartoFilterEntryPointQualified[])[] = + []; + + // Look for extension names in the filter list and result them + // into the filters provided by the extension + const getFilter = async ( + filter: QuartoFilter, + ): Promise< + QuartoFilterEntryPointQualified | QuartoFilterEntryPointQualified[] + > => { + if (filter === kQuartoFilterMarker || filter === kQuartoCiteProcMarker) { + return { type: filter }; + } + if (typeof filter !== "string") { + const path: QualifiedPath = (() => { + if (typeof filter.path !== "string") { + const result = filter.path; + return result; } - } else if (extensions.length > 0) { - // There was a matching extension with this name, but - // it was filtered out, just hide the filter altogether - return []; + const fileType: "project-relative" | "relative" = + filter.path.startsWith("/") ? "project-relative" : "relative"; + return { + type: fileType, + path: filter.path, + }; + })(); + // deno-lint-ignore no-explicit-any + if ((filter as any).at) { + const entryPoint = filter as QuartoFilterEntryPoint; + return { + ...entryPoint, + path, + }; } else { - // There were no extensions matching this name, just allow it - // through - return filter; + return { + at: "__quarto-auto", + type: filter.type, + path, + }; } - } else { - return filter; } + + // The filter string points to a file which exists + if (existsSync(filter) && !Deno.statSync(filter).isDirectory) { + const type = extname(filter) !== ".lua" ? "json" : "lua"; + return { + at: "__quarto-auto", + type, + path: { + type: "absolute", + path: resolve(filter), + }, + }; + } + + const extensions = await options.services.extension?.find( + filter, + options.source, + "filters", + options.project?.config, + options.project?.dir, + ) || []; + + const fallthroughResult = () => { + const filterType: "json" | "lua" = extname(filter) !== ".lua" + ? "json" + : "lua"; + const pathType: "project-relative" | "relative" = filter.startsWith("/") + ? "project-relative" + : "relative"; + + return { + at: "__quarto-auto", + type: filterType, + path: { + type: pathType, + path: filter, + }, + }; + }; + + if (extensions.length === 0) { + // There were no extensions matching this name, + // but the filter is a string that isn't an existing path + // this indicates that the filter is meant to be interpreted + // as a project- or file-relative path + + return fallthroughResult(); + } + + // Filter this list of extensions + const filteredExtensions = filterExtensions( + extensions || [], + filter, + "filter", + ); + + if (filteredExtensions.length === 0) { + // There was a matching extension with this name, but + // it was filtered out, just hide the filter altogether + return []; + } + + // Return any contributed plugins + // This matches an extension, use the contributed filters + const filters = extensions[0].contributes.filters; + if (!filters) { + return fallthroughResult(); + } + + // our extension-finding service returns absolute paths + // so any paths below will be "type": "absolute" + // and need no conversion + + return filters.map((f) => { + if (typeof f === "string") { + const isExistingFile = existsSync(f) && !Deno.statSync(f).isDirectory; + const type = (isExistingFile && extname(f) !== ".lua") ? "json" : "lua"; + return { + at: "__quarto-auto", + type, + path: { + type: "absolute", + path: f, + }, + }; + } + if (typeof f.path === "string") { + return { + ...f, + // deno-lint-ignore no-explicit-any + at: (f as any).at ?? "__quarto-auto", + path: { + type: "absolute", + path: f.path, + }, + }; + } + return { + ...f, + at: (f as any).at ?? "__quarto-auto", + path: f.path, + }; + }); }; + for (const filter of filters) { const r = await getFilter(filter); results.push(r); diff --git a/src/command/render/pandoc.ts b/src/command/render/pandoc.ts index 158e271402d..8919e2ad1ff 100644 --- a/src/command/render/pandoc.ts +++ b/src/command/render/pandoc.ts @@ -207,6 +207,7 @@ import { isWindows } from "../../deno_ral/platform.ts"; import { appendToCombinedLuaProfile } from "../../core/performance/perfetto-utils.ts"; import { makeTimedFunctionAsync } from "../../core/performance/function-times.ts"; import { walkJson } from "../../core/json.ts"; +import { asRawPath } from "../../core/qualified-path.ts"; // in case we are running multiple pandoc processes // we need to make sure we capture all of the trace files @@ -919,11 +920,19 @@ export async function runPandoc( allDefaults.filters = allDefaults.filters.map((filter) => { if (typeof filter === "string") { return pandocMetadataPath(filter); - } else { + } else if (typeof filter.path === "string") { return { type: filter.type, path: pandocMetadataPath(filter.path), }; + } else { + return { + type: filter.type, + path: pandocMetadataPath(asRawPath(filter.path, { + projectRoot: options.project.dir, + currentFileDir: dirname(options.source), + })), + }; } }); diff --git a/src/config/types.ts b/src/config/types.ts index 87545267822..f765049fdc9 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -277,6 +277,17 @@ export interface FormatDependency { resources?: DependencyFile[]; } +export type PathType = "project-relative" | "relative" | "absolute"; + +type BasePath = { + path: string; +}; + +export type QualifiedPath = BasePath & { type: PathType }; +export type AbsolutePath = BasePath & { type: "absolute" }; +export type RelativePath = BasePath & { type: "relative" }; +export type ProjectRelativePath = BasePath & { type: "project-relative" }; + export interface DependencyFile { name: string; path: string; @@ -350,12 +361,34 @@ export type PandocFilter = { path: string; }; -export type QuartoFilterEntryPoint = PandocFilter & { "at": string }; +export type QuartoFilterEntryPoint = PandocFilter & { at: string }; + +export type QuartoFilterEntryPointQualifiedFull = { + type: "json" | "lua"; + at: string; + path: QualifiedPath; +}; +export type QuartoFilterSpecialEntryPoint = { + type: "citeproc" | "quarto"; +}; +export type QuartoFilterEntryPointQualified = + | QuartoFilterEntryPointQualifiedFull + | QuartoFilterSpecialEntryPoint; -export type QuartoFilter = string | PandocFilter | QuartoFilterEntryPoint; +export type QuartoFilter = + | string + | PandocFilter + | QuartoFilterEntryPoint + | QuartoFilterEntryPointQualifiedFull; export function isPandocFilter(filter: QuartoFilter): filter is PandocFilter { - return ( filter).path !== undefined; + return typeof ( filter).path === "string"; +} + +export function isQuartoFilterEntryPointQualifiedFull( + filter: QuartoFilter, +): filter is QuartoFilterEntryPointQualifiedFull { + return typeof ( filter).path !== "string"; } export function isFilterEntryPoint( @@ -457,7 +490,7 @@ export interface Format { export interface LightDarkBrand { [kLight]?: Brand; - [kDark]?: Brand + [kDark]?: Brand; } export interface FormatRender { diff --git a/src/core/qualified-path.ts b/src/core/qualified-path.ts index f391acdad91..3a3bbb1d1e3 100644 --- a/src/core/qualified-path.ts +++ b/src/core/qualified-path.ts @@ -6,7 +6,13 @@ * Copyright (C) 2022 Posit Software, PBC */ -import { join, relative, resolve } from "../deno_ral/path.ts"; +import { + AbsolutePath, + ProjectRelativePath, + QualifiedPath, + RelativePath, +} from "../config/types.ts"; +import { join } from "../deno_ral/path.ts"; import { UnreachableError } from "./lib/error.ts"; export class InvalidPathError extends Error { @@ -20,23 +26,21 @@ export type PathInfo = { currentFileDir: string; }; -export type PathType = "project-relative" | "relative" | "absolute"; - -type BasePath = { - value: string; - asAbsolute: (info?: PathInfo) => AbsolutePath; - asRelative: (info?: PathInfo) => RelativePath; - asProjectRelative: (info?: PathInfo) => ProjectRelativePath; +export const asRawPath = (path: QualifiedPath, info: PathInfo): string => { + switch (path.type) { + case "absolute": + return path.path; + case "relative": + return join(info.currentFileDir, path.path); + case "project-relative": + return join(info.projectRoot, path.path); + default: + throw new UnreachableError(); + } }; -export type QualifiedPath = BasePath & { type?: PathType }; -export type AbsolutePath = BasePath & { type: "absolute" }; -export type RelativePath = BasePath & { type: "relative" }; -export type ProjectRelativePath = BasePath & { type: "project-relative" }; - export function makePath( path: string, - info?: PathInfo, forceAbsolute?: boolean, ): QualifiedPath { const type = path.startsWith("/") @@ -44,236 +48,23 @@ export function makePath( : "relative"; const result: QualifiedPath = { - value: path, + path: path, type, - asAbsolute(info?: PathInfo) { - return toAbsolutePath(this, info); - }, - asRelative(info?: PathInfo) { - return toRelativePath(this, info); - }, - asProjectRelative(info?: PathInfo) { - return toProjectRelativePath(this, info); - }, }; - // we call asAbsolute() at least once on each path so - // that the path is validated; this is simply - // so that exceptions can be raised appropriately. - const quartoPaths: PathInfo = resolvePathInfo(info); - result.asAbsolute(quartoPaths); - return result; } -export function readTextFile(t: QualifiedPath, options?: Deno.ReadFileOptions) { - return Deno.readTextFile(t.asAbsolute().value, options); -} - -export function readTextFileSync( - t: QualifiedPath, -) { - return Deno.readTextFileSync(t.asAbsolute().value); -} - -// validates an absolute path -function validate(value: string, quartoPaths: PathInfo): string { - if (!value.startsWith(quartoPaths.projectRoot)) { - throw new InvalidPathError( - "Paths cannot resolve outside of document or project root", - ); - } - return value; -} - -function toAbsolutePath( - path: QualifiedPath, - info?: PathInfo, -): AbsolutePath { - let value: string; - - if (isAbsolutePath(path)) { - return path; - } - - const quartoPaths: PathInfo = resolvePathInfo(info); - - switch (path.type) { - case "project-relative": - // project-relative -> absolute - value = resolve(join(quartoPaths.projectRoot, path.value)); - break; - case "relative": - // relative -> absolute - value = resolve(join(quartoPaths.currentFileDir, path.value)); - break; - default: - if (path.value.startsWith("/")) { - // project-relative -> absolute - value = resolve(join(quartoPaths.projectRoot, path.value)); - } else { - // relative -> absolute - value = resolve(join(quartoPaths.currentFileDir, path.value)); - } - } - value = validate(value, quartoPaths); - - return { - value, - type: "absolute", - asAbsolute(_info?: PathInfo) { - return this; - }, - asRelative(info?: PathInfo) { - return toRelativePath(this, info); - }, - asProjectRelative(info?: PathInfo) { - return toProjectRelativePath(this, info); - }, - }; -} - -function toRelativePath( - path: QualifiedPath, - info?: PathInfo, -): RelativePath { - let value: string; - - if (isRelativePath(path)) { - return path; - } - - const quartoPaths: PathInfo = resolvePathInfo(info); - - switch (path.type) { - case "absolute": - // absolute -> relative - value = relative(quartoPaths.currentFileDir, path.value); - break; - case "project-relative": { - // project-relative -> absolute -> relative - const absPath = validate( - resolve(join(quartoPaths.projectRoot, path.value)), - quartoPaths, - ); - value = relative( - quartoPaths.currentFileDir, - absPath, - ); - break; - } - default: - if (path.value.startsWith("/")) { - // project-relative -> absolute -> relative - const absPath = validate( - resolve(join(quartoPaths.projectRoot, path.value)), - quartoPaths, - ); - value = relative( - quartoPaths.currentFileDir, - absPath, - ); - } else { - throw new UnreachableError(); - } - } - - return { - value, - type: "relative", - asAbsolute(info?: PathInfo) { - return toAbsolutePath(this, info); - }, - asRelative(_info?: PathInfo) { - return this; - }, - asProjectRelative(info?: PathInfo) { - return toProjectRelativePath(this, info); - }, - }; -} - -function toProjectRelativePath( - path: QualifiedPath, - info?: PathInfo, -): ProjectRelativePath { - let value: string; - - if (isProjectRelativePath(path)) { - return path; - } - - const quartoPaths: PathInfo = resolvePathInfo(info); - - switch (path.type) { - case "absolute": - // absolute -> project-relative - value = `/${relative(quartoPaths.projectRoot, path.value)}`; - break; - case "relative": - // relative -> absolute -> project-relative - value = `/${ - relative( - quartoPaths.projectRoot, - validate( - resolve(join(quartoPaths.currentFileDir, path.value)), - quartoPaths, - ), - ) - }`; - break; - default: - if (!path.value.startsWith("/")) { - throw new UnreachableError(); - } else { - // relative -> absolute -> project-relative - value = `/${ - relative( - quartoPaths.projectRoot, - validate( - resolve(join(quartoPaths.currentFileDir, path.value)), - quartoPaths, - ), - ) - }`; - } - } - - return { - value, - type: "project-relative", - asAbsolute(info?: PathInfo) { - return toAbsolutePath(this, info); - }, - asProjectRelative(_info?: PathInfo) { - return this; - }, - asRelative(info?: PathInfo) { - return toRelativePath(this, info); - }, - }; -} - -function resolvePathInfo(path?: PathInfo): PathInfo { - if (path !== undefined) { - return path; - } - throw new Error("Unimplemented"); - // return {} as any; // FIXME this should get information from quarto's runtime. -} - -function isRelativePath(path: QualifiedPath): path is RelativePath { - return (path.type === "relative") || - (path.type === undefined && !path.value.startsWith("/")); +export function isRelativePath(path: QualifiedPath): path is RelativePath { + return path.type === "relative"; } -function isProjectRelativePath( +export function isProjectRelativePath( path: QualifiedPath, ): path is ProjectRelativePath { - return (path.type === "project-relative") || - (path.type === undefined && path.value.startsWith("/")); + return path.type === "project-relative"; } -function isAbsolutePath(path: QualifiedPath): path is AbsolutePath { +export function isAbsolutePath(path: QualifiedPath): path is AbsolutePath { return path.type === "absolute"; } diff --git a/src/extension/extension.ts b/src/extension/extension.ts index c9d3650cf9e..7b60ad74199 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -18,6 +18,7 @@ import { import { dirname, + extname, isAbsolute, join, normalize, @@ -71,6 +72,7 @@ import { resourcePath } from "../core/resources.ts"; import { warnOnce } from "../core/log.ts"; import { existsSync1 } from "../core/file.ts"; import { kFormatResources } from "../config/constants.ts"; +import { assert } from "testing/asserts"; // This is where we maintain a list of extensions that have been promoted // to 'built-in' status. If we see these extensions, we will filter them @@ -230,7 +232,7 @@ export function filterExtensions( extensionId: string, type: string, ) { - if (extensions && extensions.length > 0) { + if (extensions.length > 0) { // First see if there are now built it (quarto organization) // filters that we previously provided by quarto-ext and // filter those out @@ -757,7 +759,18 @@ async function readExtension( }); formatMeta.filters = (formatMeta.filters as QuartoFilter[] || []).flatMap( (filter) => { - return resolveFilter(embeddedExtensions, extensionDir, filter); + // that cast above is maybe invalid if filters are declared without 'type' + // deno-lint-ignore no-explicit-any + if (typeof filter === "object" && (filter as any).type === undefined) { + const unqualifiedPath = typeof filter.path === "string" + ? filter.path + : filter.path.path; + if (extname(unqualifiedPath) === ".lua") { + filter = { ...filter, type: "lua" }; + } + } + const result = resolveFilter(embeddedExtensions, extensionDir, filter); + return result; }, ); formatMeta[kRevealJSPlugins] = (formatMeta?.[kRevealJSPlugins] as Array< @@ -987,7 +1000,7 @@ function resolveFilter( // First check for the sentinel quarto filter, and allow that through // if it is present if (filter === "quarto") { - return filter; + return [filter]; } // First attempt to load this shortcode from an embedded extension @@ -1005,11 +1018,20 @@ function resolveFilter( return filters; } else { validateExtensionPath("filter", dir, filter); - return resolveFilterPath(dir, filter); + return [resolveFilterPath(dir, filter)]; } } else { - validateExtensionPath("filter", dir, filter.path); - return resolveFilterPath(dir, filter); + if (typeof filter.path === "string") { + validateExtensionPath("filter", dir, filter.path); + return [resolveFilterPath(dir, filter)]; + } else { + if (filter.path.type !== "relative") { + throw new Error( + `Failed to resolve referenced ${filter.path.path} - extensions can only declare fully-qualified paths of type "relative"`, + ); + } + return [resolveFilterPath(dir, filter.path.path)]; + } } } @@ -1027,12 +1049,27 @@ function resolveFilterPath( } else { // deno-lint-ignore no-explicit-any 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), - }; + const result: QuartoFilter = typeof filter.path === "string" + ? { + type: filter.type, + at: "__quarto-auto", + path: { + type: "absolute", + path: isAbsolute(filter.path) + ? filter.path + : join(extensionDir, filter.path), + }, + } + : { + type: filter.type, + at: "__quarto-auto", + path: { + type: "absolute", + path: isAbsolute(filter.path.path) + ? filter.path.path + : join(extensionDir, filter.path.path), + }, + }; if (filterAt === undefined) { return result; } else { diff --git a/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/.gitignore b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/.gitignore new file mode 100644 index 00000000000..075b2542afb --- /dev/null +++ b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/.gitignore @@ -0,0 +1 @@ +/.quarto/ diff --git a/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/_quarto.yml b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/_quarto.yml new file mode 100644 index 00000000000..e8cfb5f72c2 --- /dev/null +++ b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/_quarto.yml @@ -0,0 +1,21 @@ +project: + type: website + +website: + title: "fix-filter-project-paths" + navbar: + left: + - href: index.qmd + text: Home + - about.qmd + +format: + html: + theme: + - cosmo + - brand + css: styles.css + toc: true + + + diff --git a/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/about.qmd b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/about.qmd new file mode 100644 index 00000000000..07c5e7f9d13 --- /dev/null +++ b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/about.qmd @@ -0,0 +1,5 @@ +--- +title: "About" +--- + +About this site diff --git a/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/index.qmd b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/index.qmd new file mode 100644 index 00000000000..7b8e67ae3a2 --- /dev/null +++ b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/index.qmd @@ -0,0 +1,9 @@ +--- +title: "fix-filter-project-paths" +filters: + - /root.lua +--- + +This is a Quarto website. + +To learn more about Quarto websites visit . diff --git a/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/root.lua b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/root.lua new file mode 100644 index 00000000000..d20aba2d1ad --- /dev/null +++ b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/root.lua @@ -0,0 +1,4 @@ +function Pandoc(doc) + -- we just need to make sure this runs + return doc +end \ No newline at end of file diff --git a/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/styles.css b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/styles.css new file mode 100644 index 00000000000..2ddf50c7b42 --- /dev/null +++ b/tests/docs/smoke-all/2025/04/08/fix-filter-project-paths/styles.css @@ -0,0 +1 @@ +/* css styles */ diff --git a/tests/docs/smoke-all/2025/04/08/project-shortcode-paths/_quarto.yml b/tests/docs/smoke-all/2025/04/08/project-shortcode-paths/_quarto.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/docs/smoke-all/2025/04/08/project-shortcode-paths/shortcode.lua b/tests/docs/smoke-all/2025/04/08/project-shortcode-paths/shortcode.lua new file mode 100644 index 00000000000..e4753eec53f --- /dev/null +++ b/tests/docs/smoke-all/2025/04/08/project-shortcode-paths/shortcode.lua @@ -0,0 +1,5 @@ +return { + ['my-shortcode'] = function(args, kwargs, meta) + return pandoc.Str("Hello from Shorty!") + end + } \ No newline at end of file diff --git a/tests/docs/smoke-all/2025/04/08/project-shortcode-paths/subdir/hello.qmd b/tests/docs/smoke-all/2025/04/08/project-shortcode-paths/subdir/hello.qmd new file mode 100644 index 00000000000..87eeca199fe --- /dev/null +++ b/tests/docs/smoke-all/2025/04/08/project-shortcode-paths/subdir/hello.qmd @@ -0,0 +1,10 @@ +--- +title: "Hello" +format: html +shortcodes: + - /shortcode.lua +--- + +# Hello, world! + +{{< my-shortcode >}}