From 50e4316886f50cf7245609bc4a4af74e4a6b0abb Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 19 May 2020 12:59:02 +0200 Subject: [PATCH] improve MarkdownLinkRenderer - render target _blank link for all external http links - render simple a link for all links with protocol or anchor links - render local html push link for all internal links --- .../src/MarkdownLinkRenderer.test.tsx | 90 +++++++++++-------- .../src/MarkdownLinkRenderer.tsx | 64 +++++++------ 2 files changed, 90 insertions(+), 64 deletions(-) diff --git a/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx b/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx index d2a52f70e3..262fe43b31 100644 --- a/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx +++ b/scm-ui/ui-components/src/MarkdownLinkRenderer.test.tsx @@ -21,52 +21,72 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React from "react"; -import { correctLocalLink } from "./MarkdownLinkRenderer"; +import { isAnchorLink, isExternalLink, isLinkWithProtocol, createLocalLink } from "./MarkdownLinkRenderer"; -const basePath = "/repo/space/name/sources/master/"; -const pathname = basePath + "README.md/"; - -describe("correctLocalLink tests", () => { - it("should return same directory", () => { - expect(correctLocalLink(pathname, "./another.md")).toBe(basePath + "./another.md/"); - expect(correctLocalLink(pathname, "./another.md#42")).toBe(basePath + "./another.md/#42"); +describe("test isAnchorLink", () => { + it("should return true", () => { + expect(isAnchorLink("#some-thing")).toBe(true); + expect(isAnchorLink("#/some/more/complicated-link")).toBe(true); }); - it("should return same url", () => { - expect(correctLocalLink(pathname, "")).toBe(pathname); - expect(correctLocalLink(pathname, "#42")).toBe("#42"); + it("should return false", () => { + expect(isAnchorLink("https://cloudogu.com")).toBe(false); + expect(isAnchorLink("/some/path/link")).toBe(false); + }); +}); + +describe("test isExternalLink", () => { + it("should return true", () => { + expect(isExternalLink("https://cloudogu.com")).toBe(true); + expect(isExternalLink("http://cloudogu.com")).toBe(true); + }); + + it("should return false", () => { + expect(isExternalLink("some/path/link")).toBe(false); + expect(isExternalLink("/some/path/link")).toBe(false); + expect(isExternalLink("#some-anchor")).toBe(false); + expect(isExternalLink("mailto:trillian@hitchhiker.com")).toBe(false); + }); +}); + +describe("test isLinkWithProtocol", () => { + it("should return true", () => { + expect(isLinkWithProtocol("ldap://[2001:db8::7]/c=GB?objectClass?one")).toBe(true); + expect(isLinkWithProtocol("mailto:trillian@hitchhiker.com")).toBe(true); + expect(isLinkWithProtocol("tel:+1-816-555-1212")).toBe(true); + expect(isLinkWithProtocol("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")).toBe(true); + expect(isLinkWithProtocol("about:config")).toBe(true); + expect(isLinkWithProtocol("http://cloudogu.com")).toBe(true); + }); + it("should return false", () => { + expect(isExternalLink("some/path/link")).toBe(false); + expect(isExternalLink("/some/path/link")).toBe(false); + expect(isExternalLink("#some-anchor")).toBe(false); + }); +}); + +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 return main directory", () => { - expect(correctLocalLink(pathname, "/")).toBe("/"); - expect(correctLocalLink(pathname, "/users/")).toBe("/users/"); - expect(correctLocalLink(pathname, "/users/#42")).toBe("/users/#42"); + expect(createLocalLink(pathname, "/")).toBe("/"); + expect(createLocalLink(pathname, "/users/")).toBe("/users/"); + expect(createLocalLink(pathname, "/users/#42")).toBe("/users/#42"); }); it("should return ascend directory", () => { - expect(correctLocalLink(pathname, "../")).toBe(basePath + "../"); - expect(correctLocalLink(pathname, "../../")).toBe(basePath + "../../"); + expect(createLocalLink(pathname, "../")).toBe(basePath + "../"); + expect(createLocalLink(pathname, "../../")).toBe(basePath + "../../"); }); it("should return deeper links", () => { - expect(correctLocalLink(pathname, "docs/Home.md")).toBe(basePath + "docs/Home.md/"); - expect(correctLocalLink(pathname, "docs/Home.md#42")).toBe(basePath + "docs/Home.md/#42"); - }); - - it("should return external link", () => { - expect(correctLocalLink(pathname, "https://foo.bar/baz#42")).toBe("https://foo.bar/baz#42"); - expect(correctLocalLink(pathname, "ldap://[2001:db8::7]/c=GB?objectClass?one")).toBe( - "ldap://[2001:db8::7]/c=GB?objectClass?one" - ); - expect(correctLocalLink(pathname, "mailto:John.Doe@example.com")).toBe("mailto:John.Doe@example.com"); - expect(correctLocalLink(pathname, "http://userid:password@example.com:8080")).toBe( - "http://userid:password@example.com:8080" - ); - expect(correctLocalLink(pathname, "tel:+1-816-555-1212")).toBe("tel:+1-816-555-1212"); - expect(correctLocalLink(pathname, "urn:oasis:names:specification:docbook:dtd:xml:4.1.2")).toBe( - "urn:oasis:names:specification:docbook:dtd:xml:4.1.2" - ); - expect(correctLocalLink(pathname, "about:config")).toBe("about:config"); + expect(createLocalLink(pathname, "docs/Home.md")).toBe(basePath + "docs/Home.md/"); + expect(createLocalLink(pathname, "docs/Home.md#42")).toBe(basePath + "docs/Home.md/#42"); }); }); diff --git a/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx b/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx index 3b97bcedef..bc3afdf158 100644 --- a/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx +++ b/scm-ui/ui-components/src/MarkdownLinkRenderer.tsx @@ -21,68 +21,74 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, { FC, ReactNode } from "react"; +import React, { FC } from "react"; import { Link, useLocation } from "react-router-dom"; import { withContextPath } from "./urls"; +import ExternalLink from "./navigation/ExternalLink"; type Props = { - children: ReactNode; href: string; }; -const regex = new RegExp("[a-z]:"); +const externalLinkRegex = new RegExp("^http(s)?://"); +export const isExternalLink = (link: string) => { + return externalLinkRegex.test(link); +}; -/** - * Handle local SCM-Manager and external links - * - * @VisibleForTesting - */ -export function correctLocalLink(pathname: string, link: string) { - if (link === "") { - return pathname; - } +export const isAnchorLink = (link: string) => { + return link.startsWith("#"); +}; - // Leave uris unchanged which start with schemes or fragment - if (link.match(regex) || link.startsWith("#")) { - return link; - } +const linkWithProtcolRegex = new RegExp("^[a-z]+:"); +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); + 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); + if (path.startsWith("/")) { + path = path.substring(1); + } } // 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] += "/"; + if (!pathParts[0].endsWith("/")) { + pathParts[0] += "/"; + } path = pathParts[0] + "#" + pathParts[1]; - } else { - // Add ending slash - if (!path.endsWith("/")) path += "/"; + } else if (!path.endsWith("/")) { + path += "/"; } return base + path; -} +}; -const MarkdownLinkRenderer: FC = ({ children, href }) => { +const MarkdownLinkRenderer: FC = ({ href, children }) => { const location = useLocation(); - const compositeUrl = correctLocalLink(withContextPath(location.pathname), href); - - if (compositeUrl.match(regex)) { - return {children}; + if (isExternalLink(href)) { + return {children}; + } else if (isAnchorLink(href) || isLinkWithProtocol(href)) { + return {children}; + } else { + const compositeUrl = createLocalLink(withContextPath(location.pathname), href); + return {children}; } - return {children}; }; export default MarkdownLinkRenderer;