Skip to content

Commit 250ec61

Browse files
authored
Merge pull request #9953 from quarto-dev/bugfix/9948
extensions - canonicalize to array, process post-render scripts
2 parents 8d57198 + 3689615 commit 250ec61

File tree

14 files changed

+128
-56
lines changed

14 files changed

+128
-56
lines changed

news/changelog-1.5.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ All changes included in 1.5:
122122

123123
- ([#8385](https://github.com/quarto-dev/quarto-cli/issues/8385)): Properly copy project resources when extensions are installed at project level.
124124
- ([#8547](https://github.com/quarto-dev/quarto-cli/issues/8547)): Support installing extensions from github branch with forward slash in the name.
125-
- ([#9889](https://github.com/quarto-dev/quarto-cli/issues/9889)): Support `pre-render` and `post-render` script lists in `project` contributions from extensions.
125+
- ([#9948](https://github.com/quarto-dev/quarto-cli/issues/9948)): New extension type: `metadata`. Example use case: support `pre-render` and `post-render` script lists in `project` metadata.
126126

127127
## Shortcodes
128128

src/command/create/artifacts/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const kExtensionTypes: Array<string | ExtensionType> = [
4040
},
4141
{ name: "journal format", value: "journal", openfiles: ["template.qmd"] },
4242
{ name: "custom format", value: "format", openfiles: ["template.qmd"] },
43+
{ name: "metadata", value: "metadata", openfiles: [] },
4344
];
4445

4546
const kExtensionSubtypes: Record<string, string[]> = {

src/command/render/project.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { ensureDirSync, existsSync } from "fs/mod.ts";
88
import { dirname, isAbsolute, join, relative } from "../../deno_ral/path.ts";
99
import { info, warning } from "../../deno_ral/log.ts";
10+
import { mergeProjectMetadata } from "../../config/metadata.ts";
1011

1112
import * as colors from "fmt/colors.ts";
1213

@@ -72,6 +73,8 @@ import { Format } from "../../config/types.ts";
7273
import { fileExecutionEngine } from "../../execute/engine.ts";
7374
import { projectContextForDirectory } from "../../project/project-context.ts";
7475
import { ProjectType } from "../../project/types/types.ts";
76+
import { ProjectConfig as ProjectConfig_Project } from "../../resources/types/schema-types.ts";
77+
import { Extension } from "../../extension/types.ts";
7578

7679
const noMutationValidations = (
7780
projType: ProjectType,
@@ -217,14 +220,7 @@ const computeProjectRenderConfig = async (
217220

218221
const getProjectRenderScripts = async (
219222
context: ProjectContext,
220-
pOptions: RenderOptions,
221223
) => {
222-
const extensions = await pOptions.services.extension.extensions(
223-
undefined,
224-
context.config,
225-
context.isSingleFile ? undefined : context.dir,
226-
{ builtIn: false },
227-
);
228224
const preRenderScripts: string[] = [],
229225
postRenderScripts: string[] = [];
230226
if (context.config?.project?.[kProjectPreRender]) {
@@ -237,35 +233,40 @@ const getProjectRenderScripts = async (
237233
...asArray(context.config?.project?.[kProjectPostRender]!),
238234
);
239235
}
240-
extensions.forEach((extension) => {
241-
if (extension.contributes.project?.project) {
242-
const project = extension.contributes.project.project as Record<
243-
string,
244-
unknown
245-
>;
246-
if (project[kProjectPreRender]) {
247-
preRenderScripts.push(
248-
...asArray(project[kProjectPreRender] as string | string[]),
249-
);
250-
}
251-
if (project[kProjectPostRender]) {
252-
postRenderScripts.push(
253-
...asArray(project[kProjectPostRender] as string | string[]),
254-
);
255-
}
256-
}
257-
});
258236
return { preRenderScripts, postRenderScripts };
259237
};
260238

239+
const mergeExtensionMetadata = async (
240+
context: ProjectContext,
241+
pOptions: RenderOptions,
242+
) => {
243+
// this will mutate context.config.project to merge
244+
// in any project metadata from extensions
245+
if (context.config) {
246+
const extensions = await pOptions.services.extension.extensions(
247+
undefined,
248+
context.config,
249+
context.isSingleFile ? undefined : context.dir,
250+
{ builtIn: false },
251+
);
252+
const projectMetadata = extensions.map((extension) =>
253+
extension.contributes.metadata?.project
254+
).filter((project) => project) as ProjectConfig_Project[];
255+
context.config.project = mergeProjectMetadata(
256+
context.config.project,
257+
...projectMetadata,
258+
);
259+
}
260+
};
261+
261262
export async function renderProject(
262263
context: ProjectContext,
263264
pOptions: RenderOptions,
264265
pFiles?: string[],
265266
): Promise<RenderResult> {
267+
mergeExtensionMetadata(context, pOptions);
266268
const { preRenderScripts, postRenderScripts } = await getProjectRenderScripts(
267269
context,
268-
pOptions,
269270
);
270271

271272
// lookup the project type

src/extension/extension.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ function findExtensions(
347347
return true;
348348
} else if (contributes === "project" && ext.contributes.project) {
349349
return true;
350+
} else if (contributes === "metadata" && ext.contributes.metadata) {
351+
return true;
350352
} else if (
351353
contributes === kRevealJSPlugins && ext.contributes[kRevealJSPlugins]
352354
) {
@@ -625,6 +627,7 @@ function validateExtension(extension: Extension) {
625627
extension.contributes.formats,
626628
extension.contributes.project,
627629
extension.contributes[kRevealJSPlugins],
630+
extension.contributes.metadata,
628631
];
629632
contribs.forEach((contrib) => {
630633
if (contrib) {
@@ -783,22 +786,31 @@ async function readExtension(
783786
return resolveFilterPath(extensionDir, filter);
784787
},
785788
);
786-
const project = (contributes?.project || {}) as Record<string, unknown>;
787-
// resolve project pre- and post-render scripts to their full path
788-
if (
789-
project.project &&
790-
(project.project as Record<string, unknown>)["pre-render"]
791-
) {
792-
const preRender =
793-
(project.project as Record<string, unknown>)["pre-render"] as string[];
794-
const resolved = resolvePathGlobs(
795-
extensionDir,
796-
preRender as string[],
797-
[],
798-
);
799-
if (resolved.include.length > 0) {
800-
(project.project as Record<string, unknown>)["pre-render"] = resolved
801-
.include;
789+
const project = contributes?.project as Record<string, unknown> | undefined;
790+
const metadata = contributes?.metadata as Record<string, unknown> | undefined;
791+
792+
// resolve metadata/project pre- and post-render scripts to their full path
793+
for (const key of ["pre-render", "post-render"]) {
794+
for (const object of [metadata, project]) {
795+
if (!object?.project || typeof object.project !== "object") {
796+
continue;
797+
}
798+
// object.project is truthy and typeof object.project is object
799+
// so we can safely cast object.project to Record<string, unknown>
800+
// the TypeScript checker doesn't appear to recognize this
801+
const t = (object.project as Record<string, unknown>)[key];
802+
if (t) {
803+
const value = (Array.isArray(t) ? t : [t]) as string[];
804+
const resolved = resolvePathGlobs(
805+
extensionDir,
806+
value as string[],
807+
[],
808+
);
809+
if (resolved.include.length > 0) {
810+
(object.project as Record<string, unknown>)[key] = resolved
811+
.include;
812+
}
813+
}
802814
}
803815
}
804816
const revealJSPlugins = ((contributes?.[kRevealJSPlugins] || []) as Array<
@@ -816,10 +828,11 @@ async function readExtension(
816828
id: extensionId,
817829
path: extensionDir,
818830
contributes: {
831+
metadata,
819832
shortcodes,
820833
filters,
821834
formats,
822-
project,
835+
project: project ?? {},
823836
[kRevealJSPlugins]: revealJSPlugins,
824837
},
825838
};

src/extension/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export type Contributes =
2323
| "filters"
2424
| "formats"
2525
| "project"
26-
| "revealjs-plugins";
26+
| "revealjs-plugins"
27+
| "metadata";
2728

2829
export interface Extension extends Record<string, unknown> {
2930
id: ExtensionId;
@@ -33,6 +34,7 @@ export interface Extension extends Record<string, unknown> {
3334
quartoVersion?: Range;
3435
path: string;
3536
contributes: {
37+
metadata?: Metadata;
3638
project?: Record<string, unknown>;
3739
shortcodes?: string[];
3840
filters?: QuartoFilter[];
@@ -75,7 +77,8 @@ export interface ExtensionContext {
7577
| "filters"
7678
| "formats"
7779
| "project"
78-
| "revealjs-plugins",
80+
| "revealjs-plugins"
81+
| "metadata",
7982
config?: ProjectConfig,
8083
projectDir?: string,
8184
options?: ExtensionOptions,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.html
2+
*.pdf
3+
*_files/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# <%= title %> Extension For Quarto
2+
3+
_TODO_: Add a short description of your extension.
4+
5+
## Installing
6+
7+
_TODO_: Replace the `<github-organization>` with your GitHub organization.
8+
9+
```bash
10+
quarto add <github-organization>/<%= filesafename %>
11+
```
12+
13+
This will install the extension under the `_extensions` subdirectory.
14+
If you're using version control, you will want to check in this directory.
15+
16+
## Using
17+
18+
_TODO_: Describe how to use your extension.
19+
20+
## Example
21+
22+
Here is the source code for a minimal example: [example.qmd](example.qmd).
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
title: <%= title %>
2+
author: <%= author %>
3+
version: <%= version %>
4+
quarto-required: ">=<%= quartoversion %>"
5+
contributes:
6+
metadata:
7+
project:
8+
# your per-project metadata goes here

tests/docs/project/prepost/extension/_extensions/prerender/_extension.yml renamed to tests/docs/project/prepost/extension/_extensions/author/prerender/_extension.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ author: cscheid
33
version: 1.0.0
44
quarto-required: ">=1.2.0"
55
contributes:
6-
project:
6+
metadata:
77
project:
88
pre-render:
99
- pre-render.ts
10-
11-
10+
post-render:
11+
- post-render.ts
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { join } from "https://deno.land/std/path/mod.ts";
2+
3+
try {
4+
Deno.removeSync(join(Deno.cwd(), "i-exist.txt"));
5+
console.log("post-render ok");
6+
} catch (e) {
7+
if (e instanceof Deno.errors.NotFound) {
8+
throw new Error("File should exist.");
9+
} else {
10+
throw e;
11+
}
12+
}

0 commit comments

Comments
 (0)