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

🔄 Support recursive includes in myst and tex #1082

Merged
merged 8 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions .changeset/afraid-dots-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'myst-to-typst': patch
---

Add newlines after typst include/embed
6 changes: 6 additions & 0 deletions .changeset/dirty-steaks-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'myst-transforms': patch
'myst-cli': patch
---

Support tex includes directly in myst processing
7 changes: 7 additions & 0 deletions .changeset/funny-llamas-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'myst-transforms': patch
'myst-cli': patch
'mystmd': patch
---

Handle circular includes with nice errors and no infinite loops
6 changes: 6 additions & 0 deletions .changeset/sour-dancers-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'myst-transforms': patch
'mystmd': patch
---

Revive basic recursive include
10 changes: 0 additions & 10 deletions packages/myst-cli/src/process/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { createHash } from 'node:crypto';
import { tic } from 'myst-cli-utils';
import { TexParser } from 'tex-to-myst';
import { VFile } from 'vfile';
import type { GenericParent } from 'myst-common';
import { RuleId, toText } from 'myst-common';
import { validatePageFrontmatter } from 'myst-frontmatter';
import { SourceFileKind } from 'myst-spec-ext';
Expand All @@ -18,8 +17,6 @@ import { addWarningForFile } from '../utils/addWarningForFile.js';
import { loadCitations } from './citations.js';
import { parseMyst } from './myst.js';
import { processNotebook } from './notebook.js';
import { includeDirectiveTransform } from 'myst-transforms';
import { makeFileLoader } from '../transforms/include.js';
import { selectors } from '../store/index.js';

function checkCache(cache: ISessionWithCache, content: string, file: string) {
Expand Down Expand Up @@ -116,13 +113,6 @@ export async function loadFile(
const { sha256, useCache } = checkCache(cache, content, file);
if (useCache) break;
const tex = new TexParser(content, vfile);
await includeDirectiveTransform(tex.ast as GenericParent, vfile, {
Copy link
Collaborator Author

@fwkoch fwkoch Apr 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Live reload was not working well for tex include content. The fastProcessFile function was getting triggered correctly for dependent files, but this transform was never run on the file with \input{...} (useCache was still true).

Now, when this function is called in processMdast it handles tex as well, so live reload behaviour matches md processing, regardless of useCache for file loading. (And calling the function here is unnecessary.)

loadFile: makeFileLoader(session, vfile, file),
parseContent: (filename, input) => {
const subTex = new TexParser(input, vfile);
return subTex.ast.children ?? [];
},
});
const frontmatter = validatePageFrontmatter(
{
title: toText(tex.data.frontmatter.title as any),
Expand Down
85 changes: 63 additions & 22 deletions packages/myst-cli/src/transforms/include.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,64 @@ import type { VFile } from 'vfile';
import { parseMyst } from '../process/myst.js';
import type { ISession } from '../session/types.js';
import { watch } from '../store/reducers.js';
import { TexParser } from 'tex-to-myst';

export const makeFileLoader =
(session: ISession, vfile: VFile, baseFile: string) => (filename: string) => {
const dir = path.dirname(baseFile);
const fullFile = path.join(dir, filename);
/**
* Return resolveFile function
*
* If `sourceFile` is format .tex, `relativeFile` will be resolved relative to the
* original baseFile; otherwise, it will be resolved relative to `sourceFile`.
*
* The returned function will resolve the file as described above, and return it if
* it exists or log an error and return undefined otherwise.
*/
export const makeFileResolver =
(baseFile: string) => (relativeFile: string, sourceFile: string, vfile: VFile) => {
const base = sourceFile.toLowerCase().endsWith('.tex') ? baseFile : sourceFile;
const fullFile = path.resolve(path.dirname(base), relativeFile);
if (!fs.existsSync(fullFile)) {
fileError(vfile, `Include Directive: Could not find "${fullFile}" in "${baseFile}"`, {
ruleId: RuleId.includeContentLoads,
});
fileError(
vfile,
`Include Directive: Could not find "${relativeFile}" relative to "${base}"`,
{
ruleId: RuleId.includeContentLoads,
},
);
return;
}
session.store.dispatch(
watch.actions.addLocalDependency({
path: baseFile,
dependency: fullFile,
}),
);
return fs.readFileSync(fullFile).toString();
return fullFile;
};

/**
* Return loadFile function
*
* Loaded file is added to original baseFile's dependencies.
*/
export const makeFileLoader = (session: ISession, baseFile: string) => (fullFile: string) => {
session.store.dispatch(
watch.actions.addLocalDependency({
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These include dependencies do not show up in the page json "dependency" field, but they are respected for live site reloading.

I think that's ok - the json "dependencies" are used for loading data from other pages; in the case of include, this data is actually moved to the page itself.

🤔 Maybe if we update include to allow including partial snippets from md files we may need to load the rest of the data from that page...? (Similar to embed pulling in a single figure.) But for now, it's unnecessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, the included files are a local convinience, not for how the final documents work.

path: baseFile,
dependency: fullFile,
}),
);
return fs.readFileSync(fullFile).toString();
};

/**
* Return paresContent function
*
* Handles html and tex files separately; all other files are treated as MyST md.
*/
export const makeContentParser =
(session: ISession) => (filename: string, content: string, vfile: VFile) => {
if (filename.toLowerCase().endsWith('.html')) {
return [{ type: 'html', value: content }];
}
if (filename.toLowerCase().endsWith('.tex')) {
const subTex = new TexParser(content, vfile);
return subTex.ast.children ?? [];
}
return parseMyst(session, content, filename).children;
};

export async function includeFilesTransform(
Expand All @@ -33,12 +73,13 @@ export async function includeFilesTransform(
tree: GenericParent,
vfile: VFile,
) {
const parseContent = (filename: string, content: string) => {
if (filename.toLowerCase().endsWith('.html')) {
return [{ type: 'html', value: content }];
}
return parseMyst(session, content, filename).children;
};
const loadFile = makeFileLoader(session, vfile, baseFile);
await includeDirectiveTransform(tree, vfile, { loadFile, parseContent });
const parseContent = makeContentParser(session);
const loadFile = makeFileLoader(session, baseFile);
const resolveFile = makeFileResolver(baseFile);
await includeDirectiveTransform(tree, vfile, {
resolveFile,
loadFile,
parseContent,
sourceFile: baseFile,
});
}
4 changes: 2 additions & 2 deletions packages/myst-to-typst/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,10 @@ const handlers: Record<string, Handler> = {
state.write(`)`);
},
embed(node, state) {
state.renderChildren(node);
state.renderChildren(node, 2);
},
include(node, state) {
state.renderChildren(node);
state.renderChildren(node, 2);
},
footnoteReference(node, state) {
if (!node.identifier) return;
Expand Down
33 changes: 28 additions & 5 deletions packages/myst-transforms/src/include.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GenericNode, GenericParent } from 'myst-common';
import { fileWarn, RuleId } from 'myst-common';
import { fileError, fileWarn, RuleId } from 'myst-common';
import type { Code, Container, Include } from 'myst-spec-ext';
import { selectAll } from 'unist-util-select';
import type { Caption } from 'myst-spec';
Expand All @@ -8,7 +8,14 @@ import type { VFile } from 'vfile';

export type Options = {
loadFile: (filename: string) => Promise<string | undefined> | string | undefined;
parseContent: (filename: string, content: string) => Promise<GenericNode[]> | GenericNode[];
resolveFile: (includeFile: string, sourceFile: string, vfile: VFile) => string | undefined;
parseContent: (
filename: string,
content: string,
vfile: VFile,
) => Promise<GenericNode[]> | GenericNode[];
sourceFile: string;
stack?: string[];
};

/**
Expand All @@ -20,11 +27,22 @@ export type Options = {
export async function includeDirectiveTransform(tree: GenericParent, vfile: VFile, opts: Options) {
const includeNodes = selectAll('include', tree) as Include[];
if (includeNodes.length === 0) return;
if (!opts?.stack) opts.stack = [opts.sourceFile];
await Promise.all(
includeNodes.map(async (node) => {
// If the transform has already run, don't run it again!
if (node.children && node.children.length > 0) return;
const rawContent = await opts.loadFile(node.file);
const fullFile = opts.resolveFile(node.file, opts.sourceFile, vfile);
if (!fullFile) return;
// If we encounter the same include file twice in a single stack, return
if (opts.stack?.includes(fullFile)) {
fileError(vfile, `Include Directive: "${fullFile}" depends on itself`, {
ruleId: RuleId.includeContentLoads,
note: [...opts.stack, fullFile].join(' > '),
});
return;
}
const rawContent = await opts.loadFile(fullFile);
if (rawContent == null) return;
const { content, startingLineNumber } = filterIncludedContent(vfile, node.filter, rawContent);
let children: GenericNode[];
Expand Down Expand Up @@ -78,11 +96,16 @@ export async function includeDirectiveTransform(tree: GenericParent, vfile: VFil
children = [container];
}
} else {
children = await opts.parseContent(node.file, content);
children = await opts.parseContent(fullFile, content, vfile);
}
node.children = children as any;
if (!node.children?.length) return;
// Recurse!
// await includeDirectiveTransform(node as GenericParent, vfile, opts);
await includeDirectiveTransform(node as GenericParent, vfile, {
...opts,
stack: [...(opts.stack ?? []), fullFile],
sourceFile: fullFile,
});
}),
);
}
Expand Down
12 changes: 12 additions & 0 deletions packages/mystmd/tests/exports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,15 @@ cases:
outputs:
- path: acronym/_build/out.tex
content: outputs/acronym.tex
- title: Recursive includes resolve
cwd: include-recursive
command: myst build --typst
outputs:
- path: include-recursive/_build/out.typ
content: outputs/include-recursive.typ
- title: Circular includes resolve with errors
cwd: include-circular
command: myst build --typst
outputs:
- path: include-circular/_build/out.typ
content: outputs/include-circular.typ
6 changes: 6 additions & 0 deletions packages/mystmd/tests/include-circular/dir/three.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Page three

This page includes content from page one:

```{include} ../one.md
```
6 changes: 6 additions & 0 deletions packages/mystmd/tests/include-circular/dir/two.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Page two

This page includes content from page three:

```{include} ./three.md
```
6 changes: 6 additions & 0 deletions packages/mystmd/tests/include-circular/four.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Page four

This page includes content from itself:

```{include} ./four.md
```
14 changes: 14 additions & 0 deletions packages/mystmd/tests/include-circular/myst.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# See docs at: https://mystmd.org/guide/frontmatter
version: 1
project:
id: c11009ec-caf7-445c-b498-403bfe2aa61f
keywords: []
authors: []
github: https://github.com/executablebooks/mystjs
site:
template: book-theme
nav: []
actions:
- title: Learn More
url: https://mystmd.org/guide
domains: []
21 changes: 21 additions & 0 deletions packages/mystmd/tests/include-circular/one.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
export:
- _build/out.typ
---

# Page one

This page includes content from page two:

```{include} ./dir/two.md
```

and content from page three:

```{include} ./dir/three.md
```

and page four:

```{include} ./four.md
```
14 changes: 14 additions & 0 deletions packages/mystmd/tests/include-recursive/myst.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# See docs at: https://mystmd.org/guide/frontmatter
version: 1
project:
id: c11009ec-caf7-445c-b498-403bfe2aa61f
keywords: []
authors: []
github: https://github.com/executablebooks/mystjs
site:
template: book-theme
nav: []
actions:
- title: Learn More
url: https://mystmd.org/guide
domains: []
16 changes: 16 additions & 0 deletions packages/mystmd/tests/include-recursive/one.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
export:
- _build/out.typ
---

# Page one

This page includes content from page two:

```{include} ./two.md
```

and content from page three:

```{include} ./three.md
```
3 changes: 3 additions & 0 deletions packages/mystmd/tests/include-recursive/three.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Page three

This page does not include any other pages.
6 changes: 6 additions & 0 deletions packages/mystmd/tests/include-recursive/two.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Page two

This page includes content from page three:

```{include} ./three.md
```
Loading
Loading