From 6d325f56e17605477449527f496d5b9e5b4eac41 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 7 May 2019 10:11:26 +0200 Subject: [PATCH] adds option to render markdown headings with anchor links --- .../src/MarkdownHeadingRenderer.js | 35 +++++++++++++++++++ .../src/MarkdownHeadingRenderer.test.js | 18 ++++++++++ .../ui-components/src/MarkdownView.js | 16 ++++++++- 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js create mode 100644 scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js 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; }