diff --git a/.changeset/lazy-snakes-film.md b/.changeset/lazy-snakes-film.md new file mode 100644 index 0000000000..ff4097f78f --- /dev/null +++ b/.changeset/lazy-snakes-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': minor +--- + +Adds code to generate ids for headers parsed through the MarkdownContent component. diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx index 9c08b83c40..fa8cb35ca8 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx @@ -100,4 +100,34 @@ describe('', () => { 'https://example.com/blog/assets/6/header.png', ); }); + + it('render MarkdownContent component with headings given proper ids', async () => { + const rendered = await renderWithEffects( + wrapInTestApp( + , + ), + ); + + expect(rendered.getByText('Lorem ipsum').getAttribute('id')).toEqual( + 'lorem-ipsum', + ); + expect(rendered.getByText('bing bong').getAttribute('id')).toEqual( + 'bing-bong', + ); + expect( + rendered + .getByText( + 'The FitnessGram Pacer Test is a multistage aerobic capacity test', + ) + .getAttribute('id'), + ).toEqual( + 'the-fitnessgram-pacer-test-is-a-multistage-aerobic-capacity-test', + ); + }); }); diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx index 013e81817a..d97172bf75 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx @@ -20,6 +20,7 @@ import gfm from 'remark-gfm'; import React from 'react'; import { BackstageTheme } from '@backstage/theme'; import { CodeSnippet } from '../CodeSnippet'; +import { HeadingProps } from 'react-markdown/lib/ast-to-react'; export type MarkdownContentClassKey = 'markdown'; @@ -73,6 +74,21 @@ type Props = { className?: string; }; +const flatten = (text: string, child: any): string => { + if (!child) return text; + + return typeof child === 'string' + ? text + child + : React.Children.toArray(child.props.children).reduce(flatten, text); +}; + +const headingRenderer = ({ level, children }: HeadingProps) => { + const childrenArray = React.Children.toArray(children); + const text = childrenArray.reduce(flatten, ''); + const slug = text.toLocaleLowerCase('en-US').replace(/\W/g, '-'); + return React.createElement(`h${level}`, { id: slug }, children); +}; + const components: Options['components'] = { code: ({ inline, className, children, ...props }) => { const text = String(children).replace(/\n+$/, ''); @@ -85,6 +101,12 @@ const components: Options['components'] = { ); }, + h1: headingRenderer, + h2: headingRenderer, + h3: headingRenderer, + h4: headingRenderer, + h5: headingRenderer, + h6: headingRenderer, }; /**