Skip to content

Commit

Permalink
Add contentTitle remark plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
slorber committed Oct 9, 2023
1 parent f496aa5 commit f727350
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 8 deletions.
15 changes: 12 additions & 3 deletions packages/docusaurus-mdx-loader/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ function ensureMarkdownConfig(reqOptions: Options) {
}
}

/**
* data.contentTitle is set by the remark contentTitle plugin
*/
function extractContentTitleData(data: {
[key: string]: unknown;
}): string | undefined {
return data.contentTitle as string | undefined;
}

export async function mdxLoader(
this: LoaderContext<Options>,
fileString: string,
Expand Down Expand Up @@ -150,7 +159,7 @@ export async function mdxLoader(
mdxFrontMatter,
});

let result: {content: string; data: { [key: string]: unknown }};
let result: {content: string; data: {[key: string]: unknown}};
try {
result = await processor.process({content: preprocessedContent, filePath});
} catch (errorUnknown) {
Expand All @@ -176,7 +185,7 @@ export async function mdxLoader(
);
}

const contentTitle = 'xyz'; // TODO !
const contentTitle = extractContentTitleData(result.data);

// MDX partials are MDX files starting with _ or in a folder starting with _
// Partial are not expected to have associated metadata files or front matter
Expand Down Expand Up @@ -238,7 +247,7 @@ ${assets ? `export const assets = ${createAssetsExportCode(assets)};` : ''}

const code = `
${exportsCode}
${result}
${result.content}
`;

return callback(null, code);
Expand Down
12 changes: 8 additions & 4 deletions packages/docusaurus-mdx-loader/src/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import emoji from 'remark-emoji';
import headings from './remark/headings';
import contentTitle from './remark/contentTitle';
import toc from './remark/toc';
import transformImage from './remark/transformImage';
import transformLinks from './remark/transformLinks';
Expand All @@ -28,6 +29,8 @@ import type {ProcessorOptions} from '@mdx-js/mdx';
// See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
type Pluggable = any; // TODO fix this asap

type SimpleProcessorResult = {content: string; data: {[key: string]: unknown}};

// TODO alt interface because impossible to import type Processor (ESM + TS :/)
type SimpleProcessor = {
process: ({
Expand All @@ -36,7 +39,7 @@ type SimpleProcessor = {
}: {
content: string;
filePath: string;
}) => Promise<{content: string; data: { [key: string]: unknown }}>;
}) => Promise<SimpleProcessorResult>;
};

const DEFAULT_OPTIONS: MDXOptions = {
Expand Down Expand Up @@ -94,6 +97,7 @@ async function createProcessorFactory() {
...(options.beforeDefaultRemarkPlugins ?? []),
frontmatter,
directive,
[contentTitle, {removeContentTitle: options.removeContentTitle}],
...getAdmonitionsPlugins(options.admonitions ?? false),
...DEFAULT_OPTIONS.remarkPlugins,
details,
Expand Down Expand Up @@ -167,9 +171,9 @@ async function createProcessorFactory() {
path: filePath,
})
.then((vfile) => ({
content: vfile.toString(),
data: vfile.data,
})),
content: vfile.toString(),
data: vfile.data,
})),
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import plugin from '../index';

async function process(
content: string,
options: {removeContentTitle?: boolean} = {},
) {
const {remark} = await import('remark');
const processor = await remark().use({plugins: [[plugin, options]]});
return processor.process(content);
}

describe('contentTitle remark plugin', () => {
describe('extracts data.contentTitle', () => {
it('extracts h1 heading', async () => {
const result = await process(`
# contentTitle 1
## Heading Two {#custom-heading-two}
# contentTitle 2
some **markdown** *content*
`);

expect(result.data.contentTitle).toBe('contentTitle 1');
});

it('extracts h1 heading alt syntax', async () => {
const result = await process(`
contentTitle alt
===
# contentTitle 1
## Heading Two {#custom-heading-two}
# contentTitle 2
some **markdown** *content*
`);

expect(result.data.contentTitle).toBe('contentTitle alt');
});

it('works with no contentTitle', async () => {
const result = await process(`
## Heading Two {#custom-heading-two}
some **markdown** *content*
`);

expect(result.data.contentTitle).toBeUndefined();
});

it('ignore contentTitle if not in first position', async () => {
const result = await process(`
## Heading Two {#custom-heading-two}
# contentTitle 1
some **markdown** *content*
`);

expect(result.data.contentTitle).toBeUndefined();
});

it('is able to decently serialize Markdown syntax', async () => {
const result = await process(`
# some **markdown** \`content\` _italic_
some **markdown** *content*
`);

expect(result.data.contentTitle).toBe('some markdown content italic');
});
});

describe('returns appropriate content', () => {
it('returns content unmodified', async () => {
const content = `
# contentTitle 1
## Heading Two {#custom-heading-two}
# contentTitle 2
some **markdown** *content*
`.trim();

const result = await process(content);

expect(result.toString().trim()).toEqual(content);
});

it('can strip contentTitle', async () => {
const content = `
# contentTitle 1
## Heading Two {#custom-heading-two}
# contentTitle 2
some **markdown** *content*
`.trim();

const result = await process(content, {removeContentTitle: true});

expect(result.toString().trim()).toEqual(
`
## Heading Two {#custom-heading-two}
# contentTitle 2
some **markdown** *content*
`.trim(),
);
});

it('can strip contentTitle alt', async () => {
const content = `
contentTitle alt
===
## Heading Two {#custom-heading-two}
# contentTitle 2
some **markdown** *content*
`.trim();

const result = await process(content, {removeContentTitle: true});

expect(result.toString().trim()).toEqual(
`
## Heading Two {#custom-heading-two}
# contentTitle 2
some **markdown** *content*
`.trim(),
);
});
});
});
53 changes: 53 additions & 0 deletions packages/docusaurus-mdx-loader/src/remark/contentTitle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import visit, {EXIT} from 'unist-util-visit';
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
import type {Transformer} from 'unified';
import type {Heading} from 'mdast';

// TODO as of April 2023, no way to import/re-export this ESM type easily :/
// TODO upgrade to TS 5.3
// See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
// import type {Plugin} from 'unified';
type Plugin = any; // TODO fix this asap

interface PluginOptions {
removeContentTitle?: boolean;
}

/**
* A remark plugin to extract the h1 heading found in Markdown files
* This is exposed as "data.contentTitle" to the processed vfile
* Also gives the ability to strip that content title (used for the blog plugin)
*/
const plugin: Plugin = function plugin(
options: PluginOptions = {},
): Transformer {
// content title is
const removeContentTitle = options.removeContentTitle ?? false;

return async (root, vfile) => {
const {toString} = await import('mdast-util-to-string');
visit(root, 'heading', (headingNode: Heading, index, parent) => {
if (headingNode.depth === 1) {
vfile.data.contentTitle = toString(headingNode);
if (removeContentTitle) {
parent!.children.splice(index, 1);
}
return EXIT; // We only handle the very first heading
}
// We only handle contentTitle if it's the very first heading found
if (headingNode.depth >= 1) {
return EXIT;
}
return undefined;
});
};
};

export default plugin;
2 changes: 1 addition & 1 deletion packages/docusaurus-mdx-loader/src/remark/toc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {Heading, Literal} from 'mdast';
import type {Transformer} from 'unified';

// TODO as of April 2023, no way to import/re-export this ESM type easily :/
// This might change soon, likely after TS 5.2
// TODO upgrade to TS 5.3
// See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
// import type {Plugin} from 'unified';
type Plugin = any; // TODO fix this asap
Expand Down

0 comments on commit f727350

Please sign in to comment.