Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ yalc.lock

# vim swap files
*.swp

# local logs
logs
14 changes: 14 additions & 0 deletions packages/myst-cli/src/process/mdast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
checkLinkTextTransform,
indexIdentifierPlugin,
buildTocTransform,
abbreviationsListTransform,
} from 'myst-transforms';
import { unified } from 'unified';
import { select, selectAll } from 'unist-util-select';
Expand Down Expand Up @@ -98,6 +99,16 @@ const htmlHandlers = {
},
};

function collectAbbreviations(session: ISession, pageReferenceStates: ReferenceState[]) {
const cache = castSession(session);
const abbreviations: Record<string, string | null> = {};
pageReferenceStates.forEach((state) => {
const pageAbbreviations = cache.$getMdast(state.filePath)?.post?.frontmatter?.abbreviations;
if (pageAbbreviations) Object.assign(abbreviations, pageAbbreviations);
});
return abbreviations;
}

export type TransformFn = (
session: ISession,
opts: Parameters<typeof transformMdast>[1],
Expand Down Expand Up @@ -344,6 +355,9 @@ export async function postProcessMdast(
projectSlug,
mdastPost.slug,
);
abbreviationsListTransform(mdast, {

@agoose77 agoose77 Jul 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should check this — I think there's a race condition here implying that we need another sync point.

This is also true for the ToC transform I think.

Actually.... it might be fine — we read mdast only to get the custom placeholder node, which is not modified after referencing.

abbreviations: collectAbbreviations(session, pageReferenceStates),
});
}
// NOTE: This is doing things in place, we should potentially make this a different state?
const transformers = [
Expand Down
71 changes: 71 additions & 0 deletions packages/myst-directives/src/abbreviations.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, test } from 'vitest';
import type { DirectiveData } from 'myst-common';
import { abbreviationsDirective } from './abbreviations.js';
import { defaultDirectives } from './index.js';

function run(data: Partial<DirectiveData> = {}) {
return abbreviationsDirective.run!({
name: 'abbreviations',
node: {} as any,
options: {},
...data,
});
}

function log(arg: any) {
console.dir(arg, { depth: null });
return arg
}
describe('abbreviations directive', () => {
test('creates a placeholder node', () => {
expect(run()).toEqual([{ type: 'abbreviations', children: [] }]);
});

test('wraps argument content in a heading', () => {
expect(run({ arg: [{ type: 'text', value: 'Abbreviations' }] })).toEqual([
{
type: 'abbreviations',
children: [
{
type: 'heading',
depth: 2,
enumerated: false,
children: [{ type: 'text', value: 'Abbreviations' }],
},
],
},
]);
});

test('preserves heading arguments', () => {
const heading = {
type: 'heading',
depth: 3,
children: [{ type: 'text', value: 'Terms' }],
};
expect(run({ arg: [heading] })).toEqual([{ type: 'abbreviations', children: [heading] }]);
});

test('preserves common directive options', () => {
expect(
run({
options: {
class: 'compact',
label: 'abbreviations-list',
},
}),
).toEqual([
{
type: 'abbreviations',
children: [],
class: 'compact',
label: 'abbreviations-list',
identifier: 'abbreviations-list',
},
]);
});

test('is registered by default', () => {
expect(defaultDirectives).toContain(abbreviationsDirective);
});
});
33 changes: 33 additions & 0 deletions packages/myst-directives/src/abbreviations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { DirectiveData, DirectiveSpec, GenericNode } from 'myst-common';
import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js';

export const abbreviationsDirective: DirectiveSpec = {
name: 'abbreviations',
doc: 'Inserts a list of known abbreviations in the page.',
arg: {
type: 'myst',
doc: 'Heading to be included with the abbreviations list',
},
options: {
...commonDirectiveOptions('abbreviations'),
},
run(data: DirectiveData): GenericNode[] {
const children: GenericNode[] = [];
if (data.arg) {
const parsedArg = data.arg as GenericNode[];
if (parsedArg[0]?.type === 'heading') {
children.push(...parsedArg);
} else {
children.push({
type: 'heading',
depth: 2,
enumerated: false,
children: parsedArg,
});
}
}
const abbreviations = { type: 'abbreviations', children };
addCommonDirectiveOptions(data, abbreviations);
return [abbreviations];
},
};
3 changes: 3 additions & 0 deletions packages/myst-directives/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { includeDirective } from './include.js';
import { indexDirective, genIndexDirective } from './indices.js';
import { csvTableDirective, tableDirective, listTableDirective } from './table.js';
import { asideDirective } from './aside.js';
import { abbreviationsDirective } from './abbreviations.js';
import { glossaryDirective } from './glossary.js';
import { mathDirective } from './math.js';
import { mdastDirective } from './mdast.js';
Expand Down Expand Up @@ -39,6 +40,7 @@ export const defaultDirectives = [
tableDirective,
listTableDirective,
asideDirective,
abbreviationsDirective,
glossaryDirective,
mathDirective,
mdastDirective,
Expand All @@ -65,6 +67,7 @@ export { includeDirective } from './include.js';
export { indexDirective, genIndexDirective } from './indices.js';
export { csvTableDirective, listTableDirective, tableDirective } from './table.js';
export { asideDirective } from './aside.js';
export { abbreviationsDirective } from './abbreviations.js';
export { mathDirective } from './math.js';
export { mdastDirective } from './mdast.js';
export { mermaidDirective } from './mermaid.js';
Expand Down
43 changes: 42 additions & 1 deletion packages/myst-transforms/src/abbreviations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Plugin } from 'unified';
import type { GenericParent } from 'myst-common';
import type { GenericNode, GenericParent } from 'myst-common';
import { toText } from 'myst-common';
import { selectAll } from 'unist-util-select';
import type { Abbreviation, Text } from 'myst-spec';
Expand Down Expand Up @@ -80,3 +80,44 @@ export const abbreviationPlugin: Plugin<[Options], GenericParent, GenericParent>
(opts) => (tree) => {
abbreviationTransform(tree, opts);
};

export function abbreviationListChildren(abbreviations?: Record<string, string | null>) {
// turns an abbreviations object into a clean, sorted list of abbreviation entries.
const entries = Object.entries(abbreviations ?? {})
.filter((entry): entry is [string, string] => !!entry[1]) // Keeps only entries where the value exists
.sort(([a], [b]) => a.localeCompare(b)); // Sort alphabetically by abbreviation key.

if (!entries.length) return [];

return [
{
type: 'definitionList',
children: entries
.map(([abbr, title]) => [
{
type: 'definitionTerm',
children: [{ type: 'text', value: abbr }],
},
{
type: 'definitionDescription',
children: [
{
type: 'paragraph',
children: [{ type: 'text', value: title }],
},
],
},
])
.flat(),
},
];
}

export function abbreviationsListTransform(mdast: GenericParent, opts?: Options) {
const nodes = selectAll('abbreviations', mdast) as GenericNode[];
nodes.forEach((node) => {
node.type = 'block';
node.data = { ...(node.data ?? {}), part: 'abbreviations' };
node.children = [...(node.children ?? []), ...abbreviationListChildren(opts?.abbreviations)];
});
}
6 changes: 5 additions & 1 deletion packages/myst-transforms/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ export {
} from './targets.js';
export { joinGatesPlugin, joinGatesTransform } from './joinGates.js';
export { glossaryPlugin, glossaryTransform } from './glossary.js';
export { abbreviationPlugin, abbreviationTransform } from './abbreviations.js';
export {
abbreviationPlugin,
abbreviationTransform,
abbreviationsListTransform,
} from './abbreviations.js';
export { includeDirectivePlugin, includeDirectiveTransform } from './include.js';
export { containerChildrenPlugin, containerChildrenTransform } from './containers.js';
export { headingDepthPlugin, headingDepthTransform } from './headings.js';
Expand Down
81 changes: 81 additions & 0 deletions packages/myst-transforms/tests/abbreviations-list.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, expect, test } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import yaml from 'js-yaml';
import { abbreviationsListTransform } from '../src';
import { abbreviationListChildren } from '../src/abbreviations';

type TestFile = {
cases: TestCase[];
};
type TestCase = {
title: string;
before: any;
after: any;
opts?: {
abbreviations?: Record<string, string | null>;
};
};

const fixtures = path.join('tests', 'abbreviations-list.yml');
const testYaml = fs.readFileSync(fixtures).toString();
const cases = (yaml.load(testYaml) as TestFile).cases;

describe('abbreviationListChildren', () => {
test('returns no children without abbreviations', () => {
expect(abbreviationListChildren()).toEqual([]);
expect(abbreviationListChildren({})).toEqual([]);
});

test('creates a sorted definition list and omits null values', () => {
expect(
abbreviationListChildren({
MyST: 'Markedly Structured Text',
SHRILL: null,
API: 'Application Programming Interface',
}),
).toEqual([
{
type: 'definitionList',
children: [
{
type: 'definitionTerm',
children: [{ type: 'text', value: 'API' }],
},
{
type: 'definitionDescription',
children: [
{
type: 'paragraph',
children: [{ type: 'text', value: 'Application Programming Interface' }],
},
],
},
{
type: 'definitionTerm',
children: [{ type: 'text', value: 'MyST' }],
},
{
type: 'definitionDescription',
children: [
{
type: 'paragraph',
children: [{ type: 'text', value: 'Markedly Structured Text' }],
},
],
},
],
},
]);
});
});

describe('abbreviations list', () => {
test.each(cases.map((c): [string, TestCase] => [c.title, c]))(
'%s',
(_, { before, after, opts }) => {
abbreviationsListTransform(before, opts);
expect(yaml.dump(before)).toEqual(yaml.dump(after));
},
);
});
Loading