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

🔢 Enable continuous numbering across pages #1391

Merged
merged 29 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e1eea07
🔢 Respect heading numbering based on original depths
fwkoch Jul 9, 2024
f56a572
🔢 Add title numbering
fwkoch Jul 9, 2024
b946ba1
📑 Support continuous numbering across pages
fwkoch Jul 13, 2024
89779bd
Target docs
rowanc1 Jul 13, 2024
2c20233
🔧 Changeset
fwkoch Jul 14, 2024
a31f90e
🧹 Fixes to continuous numbering changes
fwkoch Jan 9, 2025
9e73822
Do not write title into enumerator
fwkoch Jan 10, 2025
b9a89b0
🔧 Get titles working with headings
fwkoch Jan 16, 2025
f63df2b
📦 Bump cffjs
fwkoch Jan 17, 2025
d67b4e2
🧪 Heading export test cases
fwkoch Jan 17, 2025
9c7ae46
🧪 Get tests working
fwkoch Jan 17, 2025
19411bf
🧪 More end-to-end tests
fwkoch Jan 17, 2025
77f247f
🔧 Add enumerators to site config
fwkoch Jan 17, 2025
4f44810
🔧 Move title offset to numbering frontmatter
fwkoch Jan 18, 2025
85e5a31
📚 Update docs for continuous numbering
fwkoch Jan 18, 2025
339bbda
🍿 Changesets
fwkoch Jan 18, 2025
454fbea
🧪 Clean webp from end-to-end tests
fwkoch Jan 18, 2025
0b5c806
🧪 Clean webp from end-to-end tests
fwkoch Jan 18, 2025
bb13b52
🧪 Clean webp from end-to-end tests
fwkoch Jan 18, 2025
5fe6246
🧪 Clean webp from end-to-end tests
fwkoch Jan 18, 2025
d276d04
🧪 Clean webp from end-to-end tests
fwkoch Jan 18, 2025
f5b38a4
🧪 Clean webp from end-to-end tests
fwkoch Jan 18, 2025
0747f6e
🧪 Clean webp from end-to-end tests
fwkoch Jan 18, 2025
a436472
🧪 Clean webp from end-to-end tests
fwkoch Jan 18, 2025
c9db0cf
remove unused type
rowanc1 Jan 20, 2025
f0e2974
🧪 More end-to-end tests for headings/titles
fwkoch Jan 22, 2025
51d6932
🧙‍♀️ Remove some heading depth magic
fwkoch Jan 22, 2025
5433556
Update packages/myst-frontmatter/src/page/validators.ts
rowanc1 Jan 23, 2025
c31f811
🔢 Add enumerator customization to numbering items (#1803)
fwkoch Jan 23, 2025
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
9 changes: 9 additions & 0 deletions .changeset/dry-moose-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'myst-frontmatter': patch
'myst-transforms': patch
'myst-spec-ext': patch
'myst-common': patch
'myst-cli': patch
---

Enable continuous numbering across pages
10 changes: 10 additions & 0 deletions .changeset/many-starfishes-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'myst-frontmatter': patch
'myst-transforms': patch
'myst-common': patch
'myst-config': patch
'myst-cli': patch
'mystmd': patch
---

Enable title numbering
6 changes: 6 additions & 0 deletions .changeset/old-houses-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'myst-frontmatter': patch
'myst-transforms': patch
---

Add enumerator customization to numbering items
6 changes: 6 additions & 0 deletions .changeset/tidy-flowers-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'myst-transforms': patch
'myst-cli': patch
---

Remove some heading depth magic
7 changes: 7 additions & 0 deletions .changeset/young-sheep-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'myst-common': patch
'myst-config': patch
'myst-cli': patch
---

Add enumerator to project config
91 changes: 79 additions & 12 deletions docs/cross-references.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,10 @@ Please see [this paragraph](#my-paragraph) and [these points](#my-points).

## Numbering

Frontmatter may specify `numbering` to customize how various components of the page are numbered. By default, numbering is enabled for figures, equations, tables, math, and code blocks; it is disabled for headings and other content types contained on the page.
Frontmatter may specify `numbering` to customize how various components of the page are numbered. By default, numbering is enabled for figures, equations, tables, math, and code blocks; it is disabled for headings and other content types contained on the page. Additionally, by default, numbering resets on every page.

### Enabling and Disabling Numbering

To enable numbering of all content, you may simply use:

```yaml
Expand All @@ -337,17 +340,10 @@ numbering:
headings: true
```

For components with numbering enabled you may specify `start` to begin counting at a number other than 1 and `template` to redefine how the component will render when referenced in the text. For this example, the figures on the page will start with `Figure 5` and when referenced in the text they will appear as "fig (5)"

```yaml
numbering:
figure:
start: 5
template: fig (%s)
```

Numbering may be used for `figure` as above, as well as `subfigure`, `equation`, `subequation`, `table`, `code`, `headings` (for all heading depths), and `heading_1` through `heading_6` (for modifying each depth separately).

### Numbering Custom Content

You may also add numbering for custom content kinds:

```markdown
Expand All @@ -365,11 +361,82 @@ This figure will be numbered as "Box 1"
:::
```

Finally, under the `numbering` object, you may specify `enumerator`. For now, this applies to all numberings on the page. Instead of enumerating as simply 1, 2, 3... they will follow the template set in `enumerator`. For example, in Appendix 1, you may want to use the following `numbering` so content is enumerated as A1.1, A1.2, A1.3...
### Customizing Numbering Appearance

By default, all components are numbered sequentially as 1, 2, 3... However, under the `numbering` object, you may specify `enumerator`. For now, this applies to all numberings on the page. Instead of enumerating as simply 1, 2, 3... they will follow the template set in `enumerator`. For example, in Appendix 1, you may want to use the following `numbering` so content is enumerated as A1.1, A1.2, A1.3...

```yaml
numbering:
enumerator: A1.%s
```

If you want to control the numbering for a specific figure, you can use the {myst:directive}`figure.enumerator` option. This will give the figure a specific enumerator, and will not increment the counting for other figures. This is helpful if you want to explicitly count figure `2a` and then carry on counting figures as normal; alternatively you can take control of numbering entirely by setting {myst:directive}`figure.enumerator` on every figure.
If you want to control the numbering for a specific figure, you can use the {myst:directive}`figure.enumerator` option. This will give the figure a specific enumerator, and will not increment the counting for other figures. This is helpful if you want to explicitly count figure `2a` and then carry on counting figures as normal; alternatively you can take control of numbering entirely by setting {myst:directive}`figure.enumerator` on every figure.

You may also redefine how the component will render when referenced in the text by specifying `template`. For this example, the figures on the page will start with `Figure 1` and when referenced in the text they will appear as "fig (1)"

```yaml
numbering:
figure:
template: fig (%s)
```

### Continuous Numbering

By default, numbering will reset on every page. However, you may enable continuous numbering across all pages by specifying `continue: true` in your `numbering` object. If this is specified on a single page, it will continue counting from the previous page; if specified in your `myst.yml`, counting will be continuous across the entire project.

```yaml
numbering:
figure:
continue: true
```

You may also override the `start` value to begin counting on the page at a specific number. This is useful, for example, if you want continuous figure numbering across your project until you reach your appendix, then you want to reset to A1:

```yaml
numbering:
enumerator: A%s
figure:
start: 1
```

### Title Numbering

By default, page titles are not numbered, even if you turn on all numbering with `numbering: true`. However, you may add `title` to your numbering object:

```yaml
numbering:
title: true
```

Doing so will enumerate all the titles in your MyST project. These numberings will follow the structure of your table of contents. So, given the structure:

```yaml
toc:
- file: index.md
- file: section_a.md
children:
- file: chapter_1.md
- file: chapter_2.md
- file: section_b.md
```

The numbering will be: `1 - index.md`, `2 - section_a.md`, `2.1 - chapter_1.md`, `2.2 - chapter_2.md`, and `3 - section_b.md`.

If both `title` and `heading` numbering are enabled, these will be incremented together. So, for the example above, if there is a single heading of level 1 in `section_a.md`, it will increment the same count as the title of `chapter_1.md` and `chapter_2.md` (i.e. the heading will be `2.1` and the chapters will be `2.2` and `2.3`):

```yaml
numbering:
title: true
headings: true
```

You can further customize this behavior by setting an explicit `offset` value on a page. By default the offset matches the nesting level in the table of contents, but, for example, if you want page numbering to be flat, despite nesting in the table of contents, you can set `offset: 0`:

```yaml
numbering:
title:
enabled: true
offset: 0
```

For the example above, the numbering will change to: `1 - index.md`, `2 - section_a.md`, `3 - chapter_1.md`, `4 - chapter_2.md`, and `5 - section_b.md`.
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/myst-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@reduxjs/toolkit": "^2.1.0",
"adm-zip": "^0.5.10",
"boxen": "^7.1.1",
"cffjs": "^0.0.1",
"cffjs": "^0.0.2",
"chalk": "^5.2.0",
"check-node-version": "^4.2.1",
"chokidar": "^3.5.3",
Expand Down
10 changes: 8 additions & 2 deletions packages/myst-cli/src/build/site/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { getSiteTemplate } from './template.js';
import { collectExportOptions } from '../utils/collectExportOptions.js';
import { filterPages } from '../../project/load.js';
import { getRawFrontmatterFromFile } from '../../process/file.js';
import { castSession } from '../../session/cache.js';

type ManifestProject = Required<SiteManifest>['projects'][0];

Expand Down Expand Up @@ -133,9 +134,10 @@ export async function localToManifestProject(
const proj = selectors.selectLocalProject(state, projectPath);
if (!proj) return null;
// Update all of the page title to the frontmatter title
const { index } = proj;
const { index, file: indexFile } = proj;
const projectFileInfo = selectors.selectFileInfo(state, proj.file);
const projectTitle = projConfig?.title || projectFileInfo.title || proj.index;
const cache = castSession(session);
const pages = await Promise.all(
proj.pages.map(async (page) => {
if ('file' in page) {
Expand All @@ -149,7 +151,8 @@ export async function localToManifestProject(
const bannerOptimized = fileInfo.bannerOptimized ?? '';
const date = fileInfo.date ?? '';
const tags = fileInfo.tags ?? [];
const { slug, level } = page;
const { slug, level, file } = page;
const { frontmatter } = cache.$getMdast(file)?.post ?? {};
const projectPage: ManifestProject['pages'][0] = {
slug,
title,
Expand All @@ -162,6 +165,7 @@ export async function localToManifestProject(
bannerOptimized,
tags,
level,
enumerator: frontmatter?.enumerator,
};
return projectPage;
}
Expand Down Expand Up @@ -191,6 +195,7 @@ export async function localToManifestProject(
session.publicPath(),
{ altOutputFolder: '/', webp: true },
);
const { frontmatter } = cache.$getMdast(indexFile)?.post ?? {};
return {
...projFrontmatter,
// TODO: a null in the project frontmatter should not fall back to index page
Expand All @@ -212,6 +217,7 @@ export async function localToManifestProject(
title: projectTitle || 'Untitled',
slug: projectSlug,
index,
enumerator: frontmatter?.enumerator,
pages,
};
}
Expand Down
53 changes: 21 additions & 32 deletions packages/myst-cli/src/process/mdast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@ import type { GenericParent, IExpressionResult, PluginUtils, References } from '
import { fileError, fileWarn, RuleId, slugToUrl } from 'myst-common';
import type { PageFrontmatter } from 'myst-frontmatter';
import { SourceFileKind } from 'myst-spec-ext';
import type { LinkTransformer } from 'myst-transforms';
import type { LinkTransformer, ReferenceState } from 'myst-transforms';
import {
basicTransformationsPlugin,
htmlPlugin,
footnotesPlugin,
ReferenceState,
MultiPageReferenceResolver,
resolveLinksAndCitationsTransform,
resolveReferencesTransform,
mathPlugin,
codePlugin,
enumerateTargetsPlugin,
keysTransform,
linksTransform,
MystTransformer,
Expand Down Expand Up @@ -97,14 +95,6 @@ export type TransformFn = (
opts: Parameters<typeof transformMdast>[1],
) => Promise<void>;

function referenceFileFromPartFile(session: ISession, partFile: string) {
const state = session.store.getState();
const partDeps = selectors.selectDependentFiles(state, partFile);
if (partDeps.length > 0) return partDeps[0];
const file = selectors.selectFileFromPart(state, partFile);
return file ?? partFile;
}

export async function transformMdast(
session: ISession,
opts: {
Expand All @@ -119,6 +109,7 @@ export async function transformMdast(
minifyMaxCharacters?: number;
index?: string;
titleDepth?: number;
offset?: number;
},
) {
const {
Expand All @@ -131,7 +122,8 @@ export async function transformMdast(
watchMode = false,
minifyMaxCharacters,
index,
titleDepth,
titleDepth, // Related to title set in markdown, rather than frontmatter
offset, // Related to multi-page nesting
execute,
} = opts;
const toc = tic();
Expand Down Expand Up @@ -167,16 +159,14 @@ export async function transformMdast(
},
projectPath,
);
if (offset) {
if (!frontmatter.numbering) frontmatter.numbering = {};
if (!frontmatter.numbering.title) frontmatter.numbering.title = {};
if (frontmatter.numbering.title.offset == null) frontmatter.numbering.title.offset = offset;
}
const references: References = {
cite: { order: [], data: {} },
};
const refFile = kind === SourceFileKind.Part ? referenceFileFromPartFile(session, file) : file;
const state = new ReferenceState(refFile, {
numbering: frontmatter.numbering,
identifiers,
vfile,
});
cache.$internalReferences[file] = state;
// Import additional content from mdast or other files
importMdastFromJson(session, file, mdast);
await includeFilesTransform(session, file, mdast, frontmatter, vfile);
Expand All @@ -192,16 +182,18 @@ export async function transformMdast(
firstDepth: (titleDepth ?? 1) + (frontmatter.content_includes_title ? 0 : 1),
})
.use(inlineMathSimplificationPlugin)
.use(mathPlugin, { macros: frontmatter.math })
.use(glossaryPlugin) // This should be before the enumerate plugins
.use(abbreviationPlugin, { abbreviations: frontmatter.abbreviations })
.use(indexIdentifierPlugin)
.use(enumerateTargetsPlugin, { state }); // This should be after math/container transforms
.use(mathPlugin, { macros: frontmatter.math });
// Load custom transform plugins
session.plugins?.transforms.forEach((t) => {
if (t.stage !== 'document') return;
pipe.use(t.plugin, undefined, pluginUtils);
});

pipe
.use(glossaryPlugin) // This should be before the enumerate plugins
.use(abbreviationPlugin, { abbreviations: frontmatter.abbreviations })
.use(indexIdentifierPlugin);

await pipe.run(mdast, vfile);

// This needs to come after basic transformations since meta tags are added there
Expand Down Expand Up @@ -272,6 +264,7 @@ export async function transformMdast(
frontmatter,
mdast,
references,
identifiers,
widgets,
} as any;
const cachedMdast = cache.$getMdast(file);
Expand All @@ -297,7 +290,7 @@ export async function postProcessMdast(
}: {
file: string;
checkLinks?: boolean;
pageReferenceStates?: ReferenceState[];
pageReferenceStates: ReferenceState[];
extraLinkTransformers?: LinkTransformer[];
},
) {
Expand All @@ -309,10 +302,7 @@ export async function postProcessMdast(
const vfile = new VFile(); // Collect errors on this file
vfile.path = file;
const { mdast, dependencies, frontmatter } = mdastPost;
const fileState = cache.$internalReferences[file];
const state = pageReferenceStates
? new MultiPageReferenceResolver(pageReferenceStates, file, vfile)
: fileState;
const state = new MultiPageReferenceResolver(pageReferenceStates, file, vfile);
const externalReferences = Object.values(cache.$externalReferences);
// NOTE: This is doing things in place, we should potentially make this a different state?
const transformers = [
Expand All @@ -327,12 +317,12 @@ export async function postProcessMdast(
new StaticFileTransformer(session, file), // Links static files and internally linked files
];
resolveLinksAndCitationsTransform(mdast, { state, transformers });
linksTransform(mdast, state.vfile as VFile, {
linksTransform(mdast, vfile, {
transformers,
selector: LINKS_SELECTOR,
});
await transformLinkedRORs(session, vfile, mdast, file);
resolveReferencesTransform(mdast, state.vfile as VFile, { state, transformers });
resolveReferencesTransform(mdast, vfile, { state, transformers });
await transformMystXRefs(session, vfile, mdast, frontmatter);
await embedTransform(session, mdast, file, dependencies, state);
const pipe = unified();
Expand All @@ -345,7 +335,6 @@ export async function postProcessMdast(
// Ensure there are keys on every node after post processing
keysTransform(mdast);
checkLinkTextTransform(mdast, externalReferences, vfile);
logMessagesFromVFile(session, fileState.vfile);
logMessagesFromVFile(session, vfile);
log.debug(toc(`Transformed mdast cross references and links for "${file}" in %s`));
if (checkLinks) await checkLinksTransform(session, file, mdast);
Expand Down
Loading
Loading