Skip to content

Enable project-relative filter paths #12494

New issue

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

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

Already on GitHub? Sign in 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