diff --git a/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js
new file mode 100644
index 0000000000..d7268c2861
--- /dev/null
+++ b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js
@@ -0,0 +1,35 @@
+// @flow
+import * as React from "react";
+
+/**
+ * Adds anchor links to markdown headings.
+ *
+ * @see Headings are missing anchors / ids
+ */
+
+type Props = {
+ children: React.Node,
+ level: number
+};
+
+function flatten(text: string, child: any) {
+ return typeof child === "string"
+ ? text + child
+ : React.Children.toArray(child.props.children).reduce(flatten, text);
+}
+
+/**
+ * Turns heading text into a anchor id
+ *
+ * @VisibleForTesting
+ */
+export function headingToAnchorId(heading: string) {
+ return heading.toLowerCase().replace(/\W/g, "-");
+}
+
+export default function MarkdownHeadingRenderer(props: Props) {
+ const children = React.Children.toArray(props.children);
+ const heading = children.reduce(flatten, "");
+ const anchorId = headingToAnchorId(heading);
+ return React.createElement("h" + props.level, {id: anchorId}, props.children);
+}
diff --git a/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js
new file mode 100644
index 0000000000..4fd8428e98
--- /dev/null
+++ b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js
@@ -0,0 +1,18 @@
+// @flow
+import React from "react";
+import { headingToAnchorId } from "./MarkdownHeadingRenderer";
+
+describe("headingToAnchorId tests", () => {
+
+ it("should lower case the text", () => {
+ expect(headingToAnchorId("Hello")).toBe("hello");
+ expect(headingToAnchorId("HeLlO")).toBe("hello");
+ expect(headingToAnchorId("HELLO")).toBe("hello");
+ });
+
+ it("should replace spaces with hyphen", () => {
+ expect(headingToAnchorId("awesome stuff")).toBe("awesome-stuff");
+ expect(headingToAnchorId("a b c d e f")).toBe("a-b-c-d-e-f");
+ });
+
+});
diff --git a/scm-ui-components/packages/ui-components/src/MarkdownView.js b/scm-ui-components/packages/ui-components/src/MarkdownView.js
index 4d2b2de92f..164c06b84a 100644
--- a/scm-ui-components/packages/ui-components/src/MarkdownView.js
+++ b/scm-ui-components/packages/ui-components/src/MarkdownView.js
@@ -3,17 +3,27 @@ import React from "react";
import SyntaxHighlighter from "./SyntaxHighlighter";
import Markdown from "react-markdown/with-html";
import {binder} from "@scm-manager/ui-extensions";
+import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer";
type Props = {
content: string,
renderContext?: Object,
renderers?: Object,
+ enableAnchorHeadings: boolean
};
class MarkdownView extends React.Component {
+ static defaultProps = {
+ enableAnchorHeadings: false
+ };
+
+ constructor(props: Props) {
+ super(props);
+ }
+
render() {
- const {content, renderers, renderContext} = this.props;
+ const {content, renderers, renderContext, enableAnchorHeadings} = this.props;
const rendererFactory = binder.getExtension("markdown-renderer-factory");
let rendererList = renderers;
@@ -26,6 +36,10 @@ class MarkdownView extends React.Component {
rendererList = {};
}
+ if (enableAnchorHeadings) {
+ rendererList.heading = MarkdownHeadingRenderer;
+ }
+
if (!rendererList.code){
rendererList.code = SyntaxHighlighter;
}