diff --git a/.changeset/old-houses-stare.md b/.changeset/old-houses-stare.md new file mode 100644 index 000000000..e349761e8 --- /dev/null +++ b/.changeset/old-houses-stare.md @@ -0,0 +1,6 @@ +--- +'myst-frontmatter': patch +'myst-transforms': patch +--- + +Add enumerator customization to numbering items diff --git a/packages/myst-frontmatter/src/numbering/numbering.yml b/packages/myst-frontmatter/src/numbering/numbering.yml index a1002d02e..5b051b0f1 100644 --- a/packages/myst-frontmatter/src/numbering/numbering.yml +++ b/packages/myst-frontmatter/src/numbering/numbering.yml @@ -42,7 +42,7 @@ cases: normalized: numbering: enumerator: - template: '' + enumerator: '' all: enabled: true figure: @@ -268,3 +268,18 @@ cases: all: enabled: true warnings: 3 + - title: enumerator validates + raw: + numbering: + title: + enumerator: '{number}' + figure: + enumerator: '{number}' + normalized: + numbering: + title: + enumerator: '{number}' + enabled: true + figure: + enumerator: '{number}' + enabled: true diff --git a/packages/myst-frontmatter/src/numbering/types.ts b/packages/myst-frontmatter/src/numbering/types.ts index bfaeb4122..c2adbd273 100644 --- a/packages/myst-frontmatter/src/numbering/types.ts +++ b/packages/myst-frontmatter/src/numbering/types.ts @@ -1,14 +1,15 @@ export type NumberingItem = { enabled?: boolean; start?: number; + enumerator?: string; template?: string; continue?: boolean; offset?: number; // only applies to title }; export type Numbering = { - enumerator?: NumberingItem; // start, enabled, and continue ignored - all?: NumberingItem; // start and template ignored + enumerator?: NumberingItem; // start, enabled, continue, and template ignored + all?: NumberingItem; // start, template, enumerator ignored title?: NumberingItem; // start, continue, and template ignored figure?: NumberingItem; subfigure?: NumberingItem; diff --git a/packages/myst-frontmatter/src/numbering/validators.ts b/packages/myst-frontmatter/src/numbering/validators.ts index 69ef45004..c076733aa 100644 --- a/packages/myst-frontmatter/src/numbering/validators.ts +++ b/packages/myst-frontmatter/src/numbering/validators.ts @@ -24,7 +24,7 @@ export const NUMBERING_KEYS = [ ...HEADING_KEYS, ]; -const NUMBERING_ITEM_KEYS = ['enabled', 'start', 'template', 'continue']; +const NUMBERING_ITEM_KEYS = ['enabled', 'start', 'enumerator', 'template', 'continue']; const CONTINUE_STRINGS = ['continue', 'next']; @@ -111,6 +111,13 @@ export function validateNumberingItem( output.enabled = output.enabled ?? true; } } + if (defined(value.enumerator)) { + const enumerator = validateString(value.enumerator, incrementOptions('enumerator', opts)); + if (defined(enumerator)) { + output.enumerator = enumerator; + output.enabled = output.enabled ?? true; + } + } if (defined(value.continue)) { const cont = validateBoolean(value.continue, incrementOptions('continue', opts)); if (defined(cont)) { @@ -121,15 +128,16 @@ export function validateNumberingItem( if (Object.keys(output).length === 0) return undefined; return output; } + export function validateTitleItem(input: any, opts: ValidationOptions): NumberingItem | undefined { if (isBoolean(input)) { input = { enabled: input }; } else if (typeof input === 'number') { input = { offset: input }; } - const value = validateObjectKeys(input, { optional: ['enabled', 'offset'] }, opts); + const value = validateObjectKeys(input, { optional: ['enabled', 'offset', 'enumerator'] }, opts); if (value === undefined) return undefined; - const output: { enabled?: boolean; offset?: number } = {}; + const output: { enabled?: boolean; offset?: number; enumerator?: string } = {}; if (defined(value.enabled)) { const enabled = validateBoolean(value.enabled, incrementOptions('enabled', opts)); if (defined(enabled)) output.enabled = enabled; @@ -146,6 +154,13 @@ export function validateTitleItem(input: any, opts: ValidationOptions): Numberin output.enabled = output.enabled ?? true; } } + if (defined(value.enumerator)) { + const enumerator = validateString(value.enumerator, incrementOptions('enumerator', opts)); + if (defined(enumerator)) { + output.enumerator = enumerator; + output.enabled = output.enabled ?? true; + } + } if (Object.keys(output).length === 0) return undefined; return output; } @@ -167,6 +182,9 @@ export function validateNumbering(input: any, opts: ValidationOptions): Numberin let headings: NumberingItem | undefined; if (defined(value.enumerator)) { const enumeratorOpts = incrementOptions('enumerator', opts); + if (typeof value.enumerator === 'string') { + value.enumerator = { enumerator: value.enumerator }; + } output.enumerator = validateNumberingItem(value.enumerator, enumeratorOpts); if (output.enumerator?.enabled != null) { if (output.enumerator.enabled !== true) { diff --git a/packages/myst-transforms/src/enumerate.spec.ts b/packages/myst-transforms/src/enumerate.spec.ts index 612f6159b..0ecf2d08c 100644 --- a/packages/myst-transforms/src/enumerate.spec.ts +++ b/packages/myst-transforms/src/enumerate.spec.ts @@ -35,6 +35,26 @@ describe('Heading counts and formatting', () => { }); describe('enumeration', () => { + test('figure enumerators', () => { + const tree = u('root', [ + u('heading', { identifier: 'ha', depth: 2 }), + u('heading', { identifier: 'hb', depth: 3 }), + u('heading', { identifier: 'hc', depth: 3 }), + u('container', { kind: 'figure', identifier: 'fig1' }), + ]); + const state = new ReferenceState('my-file.md', { + frontmatter: { + numbering: { + heading_1: { enabled: true }, + heading_2: { enabled: true }, + figure: { enumerator: 'FancyTemplateSoon.%s' }, + }, + }, + vfile: new VFile(), + }); + enumerateTargetsTransform(tree, { state }); + expect(state.getTarget('fig1')?.node.enumerator).toBe('FancyTemplateSoon.1'); + }); test('sub-equations', () => { const tree = u('root', [ u('mathGroup', { identifier: 'eq:1' }, [ @@ -51,7 +71,7 @@ describe('enumeration', () => { ]), ]); const state = new ReferenceState('my-file.md', { - frontmatter: { numbering: { enumerator: { template: 'A.%s' } } }, + frontmatter: { numbering: { enumerator: { enumerator: 'A.%s' } } }, vfile: new VFile(), }); enumerateTargetsTransform(tree, { state }); @@ -91,7 +111,7 @@ describe('enumeration', () => { u('container', { identifier: 'fig:2', kind: 'figure' }, []), ]); const state = new ReferenceState('my-file.md', { - frontmatter: { numbering: { enumerator: { template: 'A.%s' } } }, + frontmatter: { numbering: { enumerator: { enumerator: 'A.%s' } } }, vfile: new VFile(), }); enumerateTargetsTransform(tree, { state }); diff --git a/packages/myst-transforms/src/enumerate.ts b/packages/myst-transforms/src/enumerate.ts index da465e606..037634487 100644 --- a/packages/myst-transforms/src/enumerate.ts +++ b/packages/myst-transforms/src/enumerate.ts @@ -360,7 +360,7 @@ export class ReferenceState implements IReferenceStateResolver { ); this.enumerator = formatHeadingEnumerator( this.targetCounts.heading, - this.numbering.enumerator?.template, + this.numbering.title?.enumerator ?? this.numbering.enumerator?.enumerator, ); } this.identifiers = opts?.identifiers ?? []; @@ -376,7 +376,6 @@ export class ReferenceState implements IReferenceStateResolver { if (!isTargetIdentifierNode(node)) return; const kind = kindFromNode(node); const numberNode = shouldEnumerateNode(node, kind, this.numbering, this.offset); - // if (numberNode && !node.enumerator) { // Is this change a problem? if (numberNode) { this.incrementCount(node, kind as TargetKind); } @@ -400,8 +399,8 @@ export class ReferenceState implements IReferenceStateResolver { }; } - resolveEnumerator(val: any): string { - const prefix = this.numbering.enumerator?.template; + resolveEnumerator(val: any, enumerator?: string): string { + const prefix = enumerator ?? this.numbering.enumerator?.enumerator; return prefix ? prefix.replace(/%s/g, String(val)) : String(val); } @@ -427,7 +426,9 @@ export class ReferenceState implements IReferenceStateResolver { ); enumerator = formatHeadingEnumerator( this.targetCounts.heading, - this.numbering.enumerator?.template, + this.numbering[ + `heading_${node.depth - (this.numbering?.title?.enabled ? 0 : 1) + this.offset}` + ]?.enumerator ?? this.numbering.enumerator?.enumerator, ); node.enumerator = enumerator; return enumerator; @@ -442,15 +443,24 @@ export class ReferenceState implements IReferenceStateResolver { ((this.targetCounts[countKind].sub - 1) % 26) + 'a'.charCodeAt(0), ); if (node.subcontainer) { - node.parentEnumerator = this.resolveEnumerator(this.targetCounts[countKind].main); + node.parentEnumerator = this.resolveEnumerator( + this.targetCounts[countKind].main, + this.numbering[countKind]?.enumerator, + ); enumerator = letter; } else { - enumerator = this.resolveEnumerator(this.targetCounts[countKind].main + letter); + enumerator = this.resolveEnumerator( + this.targetCounts[countKind].main + letter, + this.numbering[countKind]?.enumerator, + ); } } else { this.targetCounts[kind].main += 1; this.targetCounts[kind].sub = 0; - enumerator = this.resolveEnumerator(this.targetCounts[kind].main); + enumerator = this.resolveEnumerator( + this.targetCounts[kind].main, + this.numbering[kind]?.enumerator, + ); } node.enumerator = enumerator; return enumerator; diff --git a/packages/myst-transforms/tests/enumerators.yml b/packages/myst-transforms/tests/enumerators.yml index a532a2241..e46bc4001 100644 --- a/packages/myst-transforms/tests/enumerators.yml +++ b/packages/myst-transforms/tests/enumerators.yml @@ -717,7 +717,7 @@ cases: opts: numbering: enumerator: - template: A%s + enumerator: A%s heading_1: enabled: true before: