Skip to content

Commit

Permalink
Merge pull request #7760 from quarto-dev/bugfix/1237
Browse files Browse the repository at this point in the history
resolve include shortcodes in non-executable code blocks
  • Loading branch information
cscheid authored Dec 1, 2023
2 parents 8f87011 + db2fa15 commit 0cbdcd9
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 83 deletions.
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.")

0 comments on commit 0cbdcd9

Please sign in to comment.