Skip to content

Commit 7c22539

Browse files
mmckyclaude
andcommitted
Code numbering: honour scope in book mode and template in captions
Two gaps in `code` (enumerable code-block) numbering relative to the other enumerable kinds: - `code` was missing from AUTO_PREFIX_KINDS, so `numbering.code.scope` and the book-mode chapter prefix never applied (listings enumerated 1, 2, ... instead of 1.1, 1.2, ... 2.1) - getCaptionLabel ignored the configured numbering templates, so a `template: "Listing %s"` rendered references as "Listing 1" but the caption header as "Program 1"; captions now resolve the template the same way cross-references do (defaults unchanged, since fillNumbering always supplies the built-in template) Fixes #47 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 00740bc commit 7c22539

3 files changed

Lines changed: 106 additions & 3 deletions

File tree

.changeset/code-numbering.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"myst-transforms": patch
3+
---
4+
5+
Code (enumerable code-block) numbering now matches the other enumerable kinds: `numbering.code.scope` applies the chapter/section prefix in book mode (e.g. `Listing 1.1`), and the caption noun follows the configured `numbering[kind].template` (e.g. `Listing %s`) instead of always using the built-in default, so captions and cross-references agree.

packages/myst-transforms/src/enumerate.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, test } from 'vitest';
22
import {
33
addChildrenFromTargetNode,
4+
addContainerCaptionNumbersTransform,
45
MultiPageReferenceResolver,
56
ReferenceState,
67
enumerateTargetsTransform,
@@ -1213,3 +1214,92 @@ describe('initializeTargetCounts', () => {
12131214
});
12141215
});
12151216
});
1217+
1218+
// ---------------------------------------------------------------------
1219+
// #47: code (enumerable code-block) numbering — scope + caption noun.
1220+
// ---------------------------------------------------------------------
1221+
1222+
describe('code numbering (#47)', () => {
1223+
const numbering = {
1224+
book: { enabled: true },
1225+
all: { enabled: true },
1226+
title: { enabled: true },
1227+
heading_1: { enabled: true },
1228+
code: { scope: 'chapter', template: 'Listing %s' },
1229+
};
1230+
test('code containers pick up the chapter prefix in book mode', () => {
1231+
const tree = u('root', [
1232+
u('container', { kind: 'code', identifier: 'list-1' }),
1233+
u('container', { kind: 'code', identifier: 'list-2' }),
1234+
]);
1235+
const ch1 = new ReferenceState('ch1.md', {
1236+
frontmatter: { numbering },
1237+
vfile: new VFile(),
1238+
});
1239+
enumerateTargetsTransform(tree, { state: ch1 });
1240+
expect(ch1.getTarget('list-1')?.node.enumerator).toBe('1.1');
1241+
expect(ch1.getTarget('list-2')?.node.enumerator).toBe('1.2');
1242+
// next chapter: counter resets, prefix advances
1243+
const tree2 = u('root', [u('container', { kind: 'code', identifier: 'list-3' })]);
1244+
const ch2 = new ReferenceState('ch2.md', {
1245+
frontmatter: { numbering },
1246+
previousCounts: ch1.targetCounts,
1247+
vfile: new VFile(),
1248+
});
1249+
enumerateTargetsTransform(tree2, { state: ch2 });
1250+
expect(ch2.getTarget('list-3')?.node.enumerator).toBe('2.1');
1251+
});
1252+
test('code containers honour section scope', () => {
1253+
const tree = u('root', [
1254+
u('heading', { identifier: 's1', depth: 2 }),
1255+
u('container', { kind: 'code', identifier: 'list-1' }),
1256+
u('heading', { identifier: 's2', depth: 2 }),
1257+
u('container', { kind: 'code', identifier: 'list-2' }),
1258+
]);
1259+
const state = new ReferenceState('ch1.md', {
1260+
frontmatter: {
1261+
numbering: {
1262+
...numbering,
1263+
heading_2: { enabled: true },
1264+
code: { scope: 'section', template: 'Listing %s' },
1265+
},
1266+
},
1267+
vfile: new VFile(),
1268+
});
1269+
enumerateTargetsTransform(tree, { state });
1270+
expect(state.getTarget('list-1')?.node.enumerator).toBe('1.1.1');
1271+
expect(state.getTarget('list-2')?.node.enumerator).toBe('1.2.1');
1272+
});
1273+
test('caption noun follows the configured template', () => {
1274+
const tree = u('root', [
1275+
u('container', { kind: 'code', identifier: 'list-1' }, [
1276+
u('caption', [u('paragraph', [u('text', 'My listing')])]),
1277+
]),
1278+
]);
1279+
const state = new ReferenceState('ch1.md', {
1280+
frontmatter: { numbering },
1281+
vfile: new VFile(),
1282+
});
1283+
enumerateTargetsTransform(tree, { state });
1284+
addContainerCaptionNumbersTransform(tree, new VFile(), { state });
1285+
const captionParagraph = (tree as any).children[0].children[0].children[0];
1286+
expect(captionParagraph.children[0].type).toBe('captionNumber');
1287+
expect(toText(captionParagraph.children[0])).toBe('Listing 1.1:');
1288+
});
1289+
test('caption noun falls back to the default without a template', () => {
1290+
const tree = u('root', [
1291+
u('container', { kind: 'code', identifier: 'list-1' }, [
1292+
u('caption', [u('paragraph', [u('text', 'My listing')])]),
1293+
]),
1294+
]);
1295+
const state = new ReferenceState('main.md', {
1296+
frontmatter: { numbering: { all: { enabled: true } } },
1297+
vfile: new VFile(),
1298+
});
1299+
enumerateTargetsTransform(tree, { state });
1300+
addContainerCaptionNumbersTransform(tree, new VFile(), { state });
1301+
const captionParagraph = (tree as any).children[0].children[0].children[0];
1302+
// the default template uses a non-breaking space ('Program\u00a0%s')
1303+
expect(toText(captionParagraph.children[0])).toBe('Program\u00a01:');
1304+
});
1305+
});

packages/myst-transforms/src/enumerate.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const AUTO_PREFIX_KINDS = new Set<string>([
5353
'subequation',
5454
'table',
5555
'exercise',
56+
'code',
5657
]);
5758

5859
/**
@@ -1029,11 +1030,13 @@ export const enumerateTargetsPlugin: Plugin<[StateOptions], GenericParent, Gener
10291030
enumerateTargetsTransform(tree, opts);
10301031
};
10311032

1032-
function getCaptionLabel(kind?: Container['kind'], subcontainer?: boolean) {
1033+
function getCaptionLabel(kind?: Container['kind'], subcontainer?: boolean, numbering?: Numbering) {
10331034
if (subcontainer && (kind === 'equation' || kind === 'subequation')) return `(%s)`;
10341035
if (subcontainer) return `({subEnumerator})`;
10351036
if (!kind) return 'Figure %s:';
1036-
const template = getDefaultNumberedReferenceTemplate(kind);
1037+
// The caption noun follows the configured template, matching what
1038+
// cross-references render (e.g. `numbering.code.template: "Listing %s"`)
1039+
const template = numbering?.[kind]?.template ?? getDefaultNumberedReferenceTemplate(kind);
10371040
return `${template}:`;
10381041
}
10391042

@@ -1077,10 +1080,15 @@ export function addContainerCaptionNumbersTransform(
10771080
html_id: (container as any).html_id,
10781081
enumerator: target.enumerator,
10791082
};
1083+
// Page-level resolvers need the identifier's page; a single-page
1084+
// ReferenceState carries the numbering directly
1085+
const numbering =
1086+
opts.state.resolveStateProvider(container.identifier)?.numbering ??
1087+
(opts.state as { numbering?: Numbering }).numbering;
10801088
fillReferenceEnumerators(
10811089
file,
10821090
captionNumber,
1083-
getCaptionLabel(container.kind, container.subcontainer),
1091+
getCaptionLabel(container.kind, container.subcontainer, numbering),
10841092
target,
10851093
);
10861094
// The caption number is in the paragraph, it needs a link to the figure container

0 commit comments

Comments
 (0)