From d1f10ec5a72480ff133815f2bd151001b8974196 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 19 May 2020 15:51:33 +0200 Subject: [PATCH] rebuild MarkdownLinkRenderer and added real world use cases --- .../src/MarkdownLinkRenderer.test.tsx | 43 +++++---- .../src/MarkdownLinkRenderer.tsx | 92 ++++++++++--------- scm-ui/ui-components/src/MarkdownView.tsx | 10 +- 3 files changed, 83 insertions(+), 62 deletions(-) diff --git a/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx b/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx index 262fe43b31..f6b01eb06b 100644 --- a/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx +++ b/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx @@ -66,27 +66,38 @@ describe("test isLinkWithProtocol", () => { }); describe("test createLocalLink", () => { - const basePath = "/repo/space/name/sources/master/"; - const pathname = basePath + "README.md/"; - - it("should return same directory", () => { - expect(createLocalLink(pathname, "./another.md")).toBe(basePath + "./another.md/"); - expect(createLocalLink(pathname, "./another.md#42")).toBe(basePath + "./another.md/#42"); + it("should handle relative links", () => { + const localLink = createLocalLink("/src", "/src/README.md", "docs/Home.md"); + expect(localLink).toBe("/src/docs/Home.md"); }); - it("should return main directory", () => { - expect(createLocalLink(pathname, "/")).toBe("/"); - expect(createLocalLink(pathname, "/users/")).toBe("/users/"); - expect(createLocalLink(pathname, "/users/#42")).toBe("/users/#42"); + it("should handle absolute links", () => { + const localLink = createLocalLink("/src", "/src/README.md", "/docs/Home.md"); + expect(localLink).toBe("/src/docs/Home.md"); }); - it("should return ascend directory", () => { - expect(createLocalLink(pathname, "../")).toBe(basePath + "../"); - expect(createLocalLink(pathname, "../../")).toBe(basePath + "../../"); + it("should handle relative links from locations with trailing slash", () => { + const localLink = createLocalLink("/src", "/src/README.md/", "/docs/Home.md"); + expect(localLink).toBe("/src/docs/Home.md"); }); - it("should return deeper links", () => { - expect(createLocalLink(pathname, "docs/Home.md")).toBe(basePath + "docs/Home.md/"); - expect(createLocalLink(pathname, "docs/Home.md#42")).toBe(basePath + "docs/Home.md/#42"); + it("should handle relative links from location outside of base", () => { + const localLink = createLocalLink("/src", "/info/readme", "docs/Home.md"); + expect(localLink).toBe("/src/docs/Home.md"); + }); + + it("should handle absolute links from location outside of base", () => { + const localLink = createLocalLink("/src", "/info/readme", "/docs/Home.md"); + expect(localLink).toBe("/src/docs/Home.md"); + }); + + it("should handle relative links from sub directories", () => { + const localLink = createLocalLink("/src", "/src/docs/index.md", "installation/linux.md"); + expect(localLink).toBe("/src/docs/installation/linux.md"); + }); + + it("should handle absolute links from sub directories", () => { + const localLink = createLocalLink("/src", "/src/docs/index.md", "/docs/Home.md"); + expect(localLink).toBe("/src/docs/Home.md"); }); }); diff --git a/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx b/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx index bc3afdf158..30e187265e 100644 --- a/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx +++ b/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx @@ -21,14 +21,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, { FC } from "react"; -import { Link, useLocation } from "react-router-dom"; -import { withContextPath } from "./urls"; +import React, {FC} from "react"; +import {Link, useLocation} from "react-router-dom"; import ExternalLink from "./navigation/ExternalLink"; - -type Props = { - href: string; -}; +import {withContextPath} from "./urls"; const externalLinkRegex = new RegExp("^http(s)?://"); export const isExternalLink = (link: string) => { @@ -44,51 +40,61 @@ export const isLinkWithProtocol = (link: string) => { return linkWithProtcolRegex.test(link); }; -export const createLocalLink = (pathname: string, link: string) => { - // Reference to the main directory possible if link starts with slash - let base = ""; - let path = link; - if (!link.startsWith("/")) { - base = pathname; - // Remove last slash temporary - if (base.endsWith("/")) { - base = base.substring(0, base.length - 1); - } - - // Remove current called file from path - base = base.substr(0, base.lastIndexOf("/") + 1); - - // Remove first slash for absolute consistence - if (path.startsWith("/")) { - path = path.substring(1); - } +const join = (left: string, right: string) => { + if (left.endsWith("/") && right.startsWith("/")) { + return left + right.substring(1); + } else if (!left.endsWith("/") && !right.startsWith("/")) { + return left + "/" + right; } - - // Link must end with fragment if it contains one - const pathParts = path.split("#"); - if (pathParts.length > 1) { - // Add ending slash in front of fragment - if (!pathParts[0].endsWith("/")) { - pathParts[0] += "/"; - } - path = pathParts[0] + "#" + pathParts[1]; - } else if (!path.endsWith("/")) { - path += "/"; - } - - return base + path; + return left + right; }; -const MarkdownLinkRenderer: FC = ({ href, children }) => { +export const createLocalLink = (basePath: string, currentPath: string, link: string) => { + if (link.startsWith("/")) { + return join(basePath, link); + } + if (!currentPath.startsWith(basePath)) { + return join(basePath, link); + } + let path = currentPath; + if (currentPath.endsWith("/")) { + path = currentPath.substring(0, currentPath.length - 2); + } + const lastSlash = path.lastIndexOf("/"); + if (lastSlash < 0) { + path = ""; + } else { + path = path.substring(0, lastSlash); + } + return join(path, link); +}; + +type LinkProps = { + href: string; +}; + +type Props = LinkProps & { + base: string; +}; + +const MarkdownLinkRenderer: FC = ({href, base, children}) => { const location = useLocation(); if (isExternalLink(href)) { return {children}; - } else if (isAnchorLink(href) || isLinkWithProtocol(href)) { + } else if (isLinkWithProtocol(href)) { return {children}; + } else if (isAnchorLink(href)) { + return {children}; } else { - const compositeUrl = createLocalLink(withContextPath(location.pathname), href); - return {children}; + const localLink = createLocalLink(base, location.pathname, href); + return {children}; } }; +// we use a factory method, because react-markdown does not pass +// base as prop down to our link component. +export const create = (base: string): FC => { + return props => ; +}; + export default MarkdownLinkRenderer; diff --git a/scm-ui/ui-components/src/MarkdownView.tsx b/scm-ui/ui-components/src/MarkdownView.tsx index 362e553d3b..546d1610a6 100644 --- a/scm-ui/ui-components/src/MarkdownView.tsx +++ b/scm-ui/ui-components/src/MarkdownView.tsx @@ -29,7 +29,7 @@ import { binder } from "@scm-manager/ui-extensions"; import ErrorBoundary from "./ErrorBoundary"; import SyntaxHighlighter from "./SyntaxHighlighter"; import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; -import MarkdownLinkRenderer from "./MarkdownLinkRenderer"; +import { create } from "./MarkdownLinkRenderer"; import { useTranslation } from "react-i18next"; import Notification from "./Notification"; @@ -39,6 +39,8 @@ type Props = RouteComponentProps & { renderers?: any; skipHtml?: boolean; enableAnchorHeadings?: boolean; + // basePath for markdown links + basePath?: string; }; const xmlMarkupSample = `\`\`\`xml @@ -98,7 +100,7 @@ class MarkdownView extends React.Component { } render() { - const { content, renderers, renderContext, enableAnchorHeadings, skipHtml } = this.props; + const { content, renderers, renderContext, enableAnchorHeadings, skipHtml, basePath } = this.props; const rendererFactory = binder.getExtension("markdown-renderer-factory"); let rendererList = renderers; @@ -115,7 +117,9 @@ class MarkdownView extends React.Component { rendererList.heading = MarkdownHeadingRenderer; } - rendererList.link = MarkdownLinkRenderer; + if (basePath && !rendererList.link) { + rendererList.link = create(basePath); + } if (!rendererList.code) { rendererList.code = SyntaxHighlighter;