diff --git a/.changeset/few-teams-punch.md b/.changeset/few-teams-punch.md
new file mode 100644
index 0000000000..4443c43170
--- /dev/null
+++ b/.changeset/few-teams-punch.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-components': patch
+---
+
+The MarkdownContent component now handles HTML content the same way as GitHub when rendering GitHub-flavored Markdown
diff --git a/packages/core-components/package.json b/packages/core-components/package.json
index 536f0ee9f0..72aa753bed 100644
--- a/packages/core-components/package.json
+++ b/packages/core-components/package.json
@@ -77,6 +77,7 @@
"linkify-react": "4.3.2",
"linkifyjs": "4.3.2",
"lodash": "^4.17.21",
+ "parse5": "^6.0.0",
"pluralize": "^8.0.0",
"qs": "^6.9.4",
"rc-progress": "3.5.1",
@@ -90,6 +91,8 @@
"react-use": "^17.3.2",
"react-virtualized-auto-sizer": "^1.0.11",
"react-window": "^1.8.6",
+ "rehype-raw": "^6.0.0",
+ "rehype-sanitize": "^5.0.0",
"remark-gfm": "^3.0.1",
"zen-observable": "^0.10.0",
"zod": "^3.22.4"
diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.stories.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.stories.tsx
index b6770e7d37..719f9088ba 100644
--- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.stories.tsx
+++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.stories.tsx
@@ -45,6 +45,29 @@ const markdownGithubFlavored =
'* [ ] to do\n' +
'* [x] done';
+const markdownGithubFlavoredWithHTML =
+ '# GFM with HTML\n' +
+ '\n' +
+ 'This is a paragraph with bold text and italic text.\n' +
+ '\n' +
+ '
\n' +
+ '\n' +
+ 'Here is a list:\n' +
+ '\n' +
+ '
\n' +
+ ' - First item
\n' +
+ ' - Second item with a link
\n' +
+ ' - Third item
\n' +
+ '
\n' +
+ '\n' +
+ 'And a code block:\n' +
+ '\n' +
+ '\n' +
+ 'function greet() {\n' +
+ ' console.log("Hello, world!");\n' +
+ '}\n' +
+ '
\n';
+
const markdown =
'# Choreas Iovis\n' +
'\n' +
@@ -117,3 +140,7 @@ export const MarkdownContentCommonMark = () => (
export const MarkdownContentGithubFlavoredCommonMark = () => (
);
+
+export const MarkdownContentGithubFlavoredWithHTML = () => (
+
+);
diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx
index 6f20f776e3..4dd40edcbf 100644
--- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx
+++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx
@@ -163,4 +163,52 @@ describe('', () => {
'the-fitnessgram-pacer-test-is-a-multistage-aerobic-capacity-test',
);
});
+
+ it('render MarkdownContent component with br tags for new lines in GFM dialect', async () => {
+ await renderInTestApp(
+ ,
+ );
+
+ const line1 = screen.getByText(/Line 1/);
+ const line2 = screen.getByText(/Line 2/);
+ const line3 = screen.getByText(/Line 3/);
+
+ expect(line1.nextSibling?.nodeName).toBe('BR');
+ expect(line2.previousSibling?.nodeName).toBe('BR');
+ expect(line2.nextSibling?.nodeName).toBe('BR');
+ expect(line3.previousSibling?.nodeName).toBe('BR');
+ });
+
+ it('render MarkdownContent component without allowing inline styles in GFM dialect', async () => {
+ await renderInTestApp(
+ ,
+ );
+
+ const divElement = screen.getByText(
+ 'This is a custom HTML block with inline styles.',
+ );
+ expect(divElement).toBeInTheDocument();
+ expect(divElement).not.toHaveStyle('color: blue');
+ expect(divElement).not.toHaveStyle('border: 1px solid black');
+ expect(divElement).not.toHaveStyle('padding: 10px');
+ });
+
+ it('render MarkdownContent component without disallowed elements in GFM dialect', async () => {
+ const { container } = await renderInTestApp(
+ ,
+ );
+
+ expect(screen.getByText('Safe Content')).toBeInTheDocument();
+ expect(container.querySelector('script')).toBeNull();
+ expect(container.querySelector('style')).toBeNull();
+ });
});
diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx
index 5ad23cce09..6d978ebb6f 100644
--- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx
+++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx
@@ -20,6 +20,9 @@ import gfm from 'remark-gfm';
import { Children, createElement } from 'react';
import { CodeSnippet } from '../CodeSnippet';
import { HeadingProps } from 'react-markdown/lib/ast-to-react';
+import rehypeRaw from 'rehype-raw';
+import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
+import type { PluggableList } from 'react-markdown/lib/react-markdown';
export type MarkdownContentClassKey = 'markdown';
@@ -108,6 +111,30 @@ const components: Options['components'] = {
h6: headingRenderer,
};
+const gfmRehypePlugins: PluggableList = [
+ [
+ rehypeRaw,
+ {
+ tagFiter: true,
+ },
+ ],
+ [
+ rehypeSanitize,
+ {
+ ...defaultSchema,
+ attributes: {
+ ...defaultSchema.attributes,
+ code: [
+ ...(defaultSchema.attributes?.code ?? []),
+ // for syntax highlighting classes in code blocks
+ // breaks the codesnippet component override above if omitted
+ ['className'],
+ ],
+ },
+ },
+ ],
+];
+
/**
* Renders markdown with the default dialect {@link https://github.github.com/gfm/ | gfm - GitHub flavored Markdown} to backstage theme styled HTML.
*
@@ -127,6 +154,7 @@ export function MarkdownContent(props: Props) {
return (