Skip to content
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

resolve include shortcodes in non-executable code blocks #7760

Merged
merged 1 commit into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/changelog-1.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@
- Exit if project pre or post render script fails
- Support `--output-dir` for rendering individual files.
- Use InternalError in typescript code, and offer a more helpful error message when an internal error happens.
- ([#1237](https://github.com/quarto-dev/quarto-cli/issues/1237)): Allow `include` shortcodes to be resolved from inside non-executable code cells and metadata blocks.
- ([#1392](https://github.com/quarto-dev/quarto-cli/issues/1392)): Add tools and LaTeX information to `quarto check` output.
- ([#2214](https://github.com/quarto-dev/quarto-cli/issues/2214), reopened): don't report a non-existing version of Google Chrome in macOS.
- ([#4820](https://github.com/quarto-dev/quarto-cli/issues/4820)): Add support for setting the Giscus light/dark themes.
Expand Down
35 changes: 35 additions & 0 deletions src/core/handlers/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import { mappedStringFromFile } from "../mapped-text.ts";
import { error } from "log/mod.ts";
import { withCriClient } from "../cri/cri.ts";
import { normalizePath } from "../path.ts";
import { isBlockShortcode } from "../lib/parse-shortcode.ts";
import { standaloneInclude } from "./include-standalone.ts";

const handlers: Record<string, LanguageHandler> = {};

Expand Down Expand Up @@ -311,6 +313,34 @@ export function install(handler: LanguageHandler) {
}
}

const processMarkdownIncludes = async (
newCells: MappedString[],
options: LanguageCellHandlerOptions,
) => {
const includeHandler = makeHandlerContext({
...options,
});
// search for include shortcodes in the cell content
for (let i = 0; i < newCells.length; ++i) {
const lines = mappedLines(newCells[i], true);
let foundShortcodes = false;
for (let j = 0; j < lines.length; ++j) {
const shortcode = isBlockShortcode(lines[j].value);
if (shortcode && shortcode.name === "include") {
foundShortcodes = true;
const param = shortcode.params[0];
if (!param) {
throw new Error("Include directive needs filename as a parameter");
}
lines[j] = await standaloneInclude(includeHandler.context, param);
}
}
if (foundShortcodes) {
newCells[i] = mappedConcat(lines);
}
}
};

export async function handleLanguageCells(
options: LanguageCellHandlerOptions,
): Promise<{
Expand Down Expand Up @@ -438,6 +468,11 @@ export async function handleLanguageCells(
}
}
}

// now handle the markdown content. This is necessary specifically for
// include shortcodes that can still be hiding inside of code blocks
await processMarkdownIncludes(newCells, options);

return {
markdown: mappedJoin(newCells, ""),
results,
Expand Down
92 changes: 92 additions & 0 deletions src/core/handlers/include-standalone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* include-standalone.ts
*
* Copyright (C) 2023 Posit Software, PBC
*/

import { LanguageCellHandlerContext } from "./types.ts";

import {
asMappedString,
EitherString,
mappedConcat,
MappedString,
mappedString,
} from "../lib/mapped-text.ts";

import { rangedLines } from "../lib/ranged-text.ts";
import { isBlockShortcode } from "../lib/parse-shortcode.ts";

export const standaloneInclude = async (
handlerContext: LanguageCellHandlerContext,
filename: string,
): Promise<MappedString> => {
const source = handlerContext.options.context.target.source;
const retrievedFiles: string[] = [source];

const textFragments: EitherString[] = [];

const retrieveInclude = async (filename: string) => {
const path = handlerContext.resolvePath(filename);

if (retrievedFiles.indexOf(path) !== -1) {
throw new Error(
`Include directive found circular include of file ${filename}.`,
);
}

let includeSrc;
try {
includeSrc = asMappedString(
Deno.readTextFileSync(path),
path,
);
} catch (_e) {
const errMsg: string[] = [`Include directive failed.`];
errMsg.push(...retrievedFiles.map((s) => ` in file ${s}, `));
errMsg.push(
` could not find file ${path
// relative(handlerContext.options.context.target.source, path)
}.`,
);
throw new Error(errMsg.join("\n"));
}

retrievedFiles.push(filename);

let rangeStart = 0;
for (const { substring, range } of rangedLines(includeSrc.value)) {
const m = isBlockShortcode(substring);
if (m && m.name.toLocaleLowerCase() === "include") {
textFragments.push(
mappedString(includeSrc, [{
start: rangeStart,
end: range.start,
}]),
);
rangeStart = range.end;
const params = m.params;
if (params.length === 0) {
throw new Error("Include directive needs file parameter");
}

await retrieveInclude(params[0]);
}
}
if (rangeStart !== includeSrc.value.length) {
textFragments.push(
mappedString(includeSrc, [{
start: rangeStart,
end: includeSrc.value.length,
}]),
);
}
textFragments.push(includeSrc.value.endsWith("\n") ? "\n" : "\n\n");

retrievedFiles.pop();
};

await retrieveInclude(filename);

return Promise.resolve(mappedConcat(textFragments));
};
90 changes: 7 additions & 83 deletions src/core/handlers/include.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
/*
* include.ts
*
* Copyright (C) 2022 Posit Software, PBC
*
*/
* include.ts
*
* Copyright (C) 2022 Posit Software, PBC
*/

import { LanguageCellHandlerContext, LanguageHandler } from "./types.ts";
import { baseHandler, install } from "./base.ts";
import {
asMappedString,
EitherString,
mappedConcat,
MappedString,
mappedString,
} from "../lib/mapped-text.ts";
import { MappedString } from "../lib/mapped-text.ts";

import { rangedLines } from "../lib/ranged-text.ts";
import { isBlockShortcode } from "../lib/parse-shortcode.ts";
import { DirectiveCell } from "../lib/break-quarto-md-types.ts";
import { standaloneInclude } from "./include-standalone.ts";

const includeHandler: LanguageHandler = {
...baseHandler,
Expand All @@ -31,79 +23,11 @@ const includeHandler: LanguageHandler = {
handlerContext: LanguageCellHandlerContext,
directive: DirectiveCell,
): Promise<MappedString> {
const source = handlerContext.options.context.target.source;
const retrievedFiles: string[] = [source];

const textFragments: EitherString[] = [];

const retrieveInclude = async (filename: string) => {
const path = handlerContext.resolvePath(filename);

if (retrievedFiles.indexOf(path) !== -1) {
throw new Error(
`Include directive found circular include of file ${filename}.`,
);
}

let includeSrc;
try {
includeSrc = asMappedString(
Deno.readTextFileSync(path),
path,
);
} catch (_e) {
const errMsg: string[] = [`Include directive failed.`];
errMsg.push(...retrievedFiles.map((s) => ` in file ${s}, `));
errMsg.push(
` could not find file ${path
// relative(handlerContext.options.context.target.source, path)
}.`,
);
throw new Error(errMsg.join("\n"));
}

retrievedFiles.push(filename);

let rangeStart = 0;
for (const { substring, range } of rangedLines(includeSrc.value)) {
const m = isBlockShortcode(substring);
if (m && m.name.toLocaleLowerCase() === "include") {
textFragments.push(
mappedString(includeSrc, [{
start: rangeStart,
end: range.start,
}]),
);
rangeStart = range.end;
const params = m.params;
if (params.length === 0) {
throw new Error("Include directive needs file parameter");
}

await retrieveInclude(params[0]);
}
}
if (rangeStart !== includeSrc.value.length) {
textFragments.push(
mappedString(includeSrc, [{
start: rangeStart,
end: includeSrc.value.length,
}]),
);
}
textFragments.push(includeSrc.value.endsWith("\n") ? "\n" : "\n\n");

retrievedFiles.pop();
};

const param = directive.shortcode.params[0];
if (!param) {
throw new Error("Include directive needs filename as a parameter");
}

await retrieveInclude(param);

return Promise.resolve(mappedConcat(textFragments));
return await standaloneInclude(handlerContext, param);
},
};

Expand Down
19 changes: 19 additions & 0 deletions tests/docs/smoke-all/2023/12/01/1237.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
format: html
_quarto:
tests:
html:
ensureFileRegexMatches:
- ["Hello"]
- []
---

```.r
{{< include file.r >}}
```


```{r}
# Don't do anything here.
cat("world.")
```
1 change: 1 addition & 0 deletions tests/docs/smoke-all/2023/12/01/file.r
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cat("Hello.")