Skip to content

Enable project-relative filter paths #12494

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
37 changes: 29 additions & 8 deletions src/command/preview/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
267 changes: 196 additions & 71 deletions src/command/render/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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";

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<QuartoFilterSpec | undefined> {
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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<QuartoFilter[]> {
// 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<QuartoFilterEntryPointQualified[]> {
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);
Expand Down
Loading
Loading