From cb9000ede8a6ce5f2b96ffa0e87257b80260a199 Mon Sep 17 00:00:00 2001 From: Arctic Ice Studio Date: Thu, 6 Dec 2018 16:28:47 +0100 Subject: [PATCH] Implement util functions to match internal routes To handle the logic of conditionally rendering a Gatsby Link or a basic ``, based on the passed target URL (internal & external), this commit implements the `isRouteInternal` and `isRoutePartiallyMatch` functions to evaluate the passed target URL. Associated epic: GH-69 GH-70 --- src/config/routes/constants.js | 18 +++++++++ src/config/routes/mappings.js | 24 ++++++++++-- src/utils/index.js | 4 +- src/utils/isRouteInternal.js | 24 ++++++++++++ src/utils/isRoutePartiallyMatch.js | 37 +++++++++++++++++++ test/utils/isRouteInternal.test.js | 41 +++++++++++++++++++++ test/utils/isRoutePartiallyMatch.test.js | 47 ++++++++++++++++++++++++ 7 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 src/utils/isRouteInternal.js create mode 100644 src/utils/isRoutePartiallyMatch.js create mode 100644 test/utils/isRouteInternal.test.js create mode 100644 test/utils/isRoutePartiallyMatch.test.js diff --git a/src/config/routes/constants.js b/src/config/routes/constants.js index 6953bf10..4b6695e7 100644 --- a/src/config/routes/constants.js +++ b/src/config/routes/constants.js @@ -50,6 +50,14 @@ const ROOT = pathSeparator; */ const BLOG = "blog"; +/** + * The route name of the "community" page. + * + * @constant {string} + * @since 0.3.0 + */ +const COMMUNITY = "community"; + /** * The route name of the "docs" page. * @@ -67,10 +75,20 @@ const DOCS = "docs"; */ const LANDING = "landing"; +/** + * The route name of the port projects page. + * + * @constant {string} + * @since 0.3.0 + */ +const PORTS = "ports"; + module.exports = { BASE_PUBLIC_URL, BLOG, + COMMUNITY, DOCS, LANDING, + PORTS, ROOT }; diff --git a/src/config/routes/mappings.js b/src/config/routes/mappings.js index f2e2aecb..ee2b6cd2 100644 --- a/src/config/routes/mappings.js +++ b/src/config/routes/mappings.js @@ -14,7 +14,7 @@ * @since 0.1.0 */ -const { ROOT, BLOG, DOCS, LANDING } = require("./constants"); +const { BLOG, COMMUNITY, DOCS, LANDING, PORTS, ROOT } = require("./constants"); /** * The root route mapping. @@ -32,6 +32,14 @@ const ROUTE_ROOT = ROOT; */ const ROUTE_BLOG = ROUTE_ROOT + BLOG; +/** + * The route mapping for the "community" page. + * + * @constant {string} + * @since 0.3.0 + */ +const ROUTE_COMMUNITY = ROUTE_ROOT + COMMUNITY; + /** * The route mapping for the "docs" page. * @@ -49,9 +57,19 @@ const ROUTE_DOCS = ROUTE_ROOT + DOCS; */ const ROUTE_LANDING = ROUTE_ROOT + LANDING; +/** + * The route mapping for the port projects page. + * + * @constant {string} + * @since 0.3.0 + */ +const ROUTE_PORTS = ROUTE_ROOT + PORTS; + module.exports = { - ROUTE_ROOT, ROUTE_BLOG, + ROUTE_COMMUNITY, ROUTE_DOCS, - ROUTE_LANDING + ROUTE_LANDING, + ROUTE_PORTS, + ROUTE_ROOT }; diff --git a/src/utils/index.js b/src/utils/index.js index ce9b4653..c895b1b0 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -14,6 +14,8 @@ * @since 0.2.0 */ +import isRouteInternal from "./isRouteInternal"; +import isRoutePartiallyMatch from "./isRoutePartiallyMatch"; import { readSessionCache, writeSessionCache } from "./sessionCache"; -export { readSessionCache, writeSessionCache }; +export { isRouteInternal, isRoutePartiallyMatch, readSessionCache, writeSessionCache }; diff --git a/src/utils/isRouteInternal.js b/src/utils/isRouteInternal.js new file mode 100644 index 00000000..c4117fa2 --- /dev/null +++ b/src/utils/isRouteInternal.js @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018-present Arctic Ice Studio + * Copyright (C) 2018-present Sven Greb + * + * Project: Nord Docs + * Repository: https://github.com/arcticicestudio/nord-docs + * License: MIT + */ + +/* eslint-disable no-useless-escape */ + +/** + * Validates if the given route is internal. + * Matches exactly one slash or hash, anything else is external including relative routes starting with two slahes. + * The hash allows to link to anchors within the same document. + * + * @method isRouteInternal + * @param {string} route The route to validate. + * @return {Boolean} `true` if the given route is internal, `false` otherwise. + * @since 0.3.0 + */ +const isRouteInternal = route => /^[\/#](?!\/)/.test(route); + +export default isRouteInternal; diff --git a/src/utils/isRoutePartiallyMatch.js b/src/utils/isRoutePartiallyMatch.js new file mode 100644 index 00000000..bc27448d --- /dev/null +++ b/src/utils/isRoutePartiallyMatch.js @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018-present Arctic Ice Studio + * Copyright (C) 2018-present Sven Greb + * + * Project: Nord Docs + * Repository: https://github.com/arcticicestudio/nord-docs + * License: MIT + */ + +import isRouteInternal from "./isRouteInternal"; + +/** + * Validates if the given path element partially matches the route. + * + * @method isRoutePartiallyMatch + * @param {string} route The route to check. + * @param {string} pathElement The path element to check against the route. + * @return {Boolean} `true` if the given path element is partially matching, `false` otherwise. + * @since 0.3.0 + */ +const isRoutePartiallyMatch = (route, pathElement) => { + /* Don't match exact and external routes. */ + if (route === pathElement) return false; + if (!isRouteInternal(pathElement)) return false; + + /* Split into path elements and filter out leading and pending slashes. */ + const routeTokens = route.split("/").filter(t => t.length); + const pathElementTokens = pathElement.split("/").filter(t => t.length); + + const isMatch = pathElementTokens.every((t, idx) => routeTokens[idx] === t); + /* Prevent false-positive match by only allowing the path element as exact root when current route is not the root. */ + const isPathElementExactRoot = pathElement === "/" && route !== "/"; + + return isPathElementExactRoot ? false : isMatch; +}; + +export default isRoutePartiallyMatch; diff --git a/test/utils/isRouteInternal.test.js b/test/utils/isRouteInternal.test.js new file mode 100644 index 00000000..56189d79 --- /dev/null +++ b/test/utils/isRouteInternal.test.js @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018-present Arctic Ice Studio + * Copyright (C) 2018-present Sven Greb + * + * Project: Nord Docs + * Repository: https://github.com/arcticicestudio/nord-docs + * License: MIT + */ + +import { isRouteInternal } from "utils"; +import { ROUTE_BLOG, ROUTE_DOCS, ROUTE_COMMUNITY, ROUTE_PORTS, ROUTE_ROOT } from "config/routes/mappings"; +import { metadataNordDocs } from "data/project"; + +describe("internal routes are", () => { + test("matching", () => { + [ + "#", + ROUTE_ROOT, + `${ROUTE_ROOT}#`, + `${ROUTE_ROOT}?port=atom`, + ROUTE_BLOG, + ROUTE_DOCS, + ROUTE_COMMUNITY, + ROUTE_PORTS + ].forEach(route => expect(isRouteInternal(route)).toBeTruthy()); + }); + + test("not matching", () => { + [ + `${metadataNordDocs.homepage}`, + `${metadataNordDocs.repository.url}`, + "https://github.com/arcticicestudio", + "https://www.nordtheme.com", + "https://nordtheme.com", + "https://nordtheme.com", + "//nordtheme.com", + "file:///etc/hosts", + "mailto:support@nordtheme.com" + ].forEach(route => expect(isRouteInternal(route)).toBeFalsy()); + }); +}); diff --git a/test/utils/isRoutePartiallyMatch.test.js b/test/utils/isRoutePartiallyMatch.test.js new file mode 100644 index 00000000..d82159b9 --- /dev/null +++ b/test/utils/isRoutePartiallyMatch.test.js @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018-present Arctic Ice Studio + * Copyright (C) 2018-present Sven Greb + * + * Project: Nord Docs + * Repository: https://github.com/arcticicestudio/nord-docs + * License: MIT + */ + +import { isRoutePartiallyMatch } from "utils"; +import { ROUTE_BLOG, ROUTE_DOCS, ROUTE_COMMUNITY, ROUTE_PORTS, ROUTE_ROOT } from "config/routes/mappings"; +import { metadataNordDocs } from "data/project"; + +describe("partial routes are", () => { + test("matching", () => { + [ + { route: `${ROUTE_BLOG}/2018/12/06/snow-winter`, pathElement: ROUTE_BLOG }, + { route: `${ROUTE_DOCS}${ROUTE_PORTS}/vim`, pathElement: ROUTE_DOCS }, + { route: `${ROUTE_COMMUNITY}/slack`, pathElement: ROUTE_COMMUNITY } + ].forEach(({ route, pathElement }) => expect(isRoutePartiallyMatch(route, pathElement)).toBeTruthy()); + }); + + test("not matching", () => { + [ + { route: ROUTE_ROOT, pathElement: ROUTE_ROOT }, + { route: ROUTE_ROOT, pathElement: ROUTE_BLOG }, + { route: `${ROUTE_BLOG}/2018`, pathElement: `${ROUTE_BLOG}/2018/12/06/snow-winter` }, + { route: `${ROUTE_BLOG}/2018/12/06/snow-winter`, pathElement: `${ROUTE_BLOG}/2018/12/06/snow-winter` } + ].forEach(({ route, pathElement }) => expect(isRoutePartiallyMatch(route, pathElement)).toBeFalsy()); + }); + + test("not matching exact and external routes", () => { + [ + { route: ROUTE_ROOT, pathElement: ROUTE_ROOT }, + { route: metadataNordDocs.homepage, pathElement: ROUTE_DOCS }, + { route: metadataNordDocs.repository.url, pathElement: ROUTE_COMMUNITY }, + { route: "https://www.nordtheme.com", pathElement: ROUTE_ROOT } + ].forEach(({ route, pathElement }) => expect(isRoutePartiallyMatch(route, pathElement)).toBeFalsy()); + }); +}); + +test("prevents false-positive match by only allowing the path element as exact root when current route is not the root", () => { + [ + { route: ROUTE_ROOT, pathElement: ROUTE_BLOG }, + { route: ROUTE_ROOT, pathElement: `${ROUTE_BLOG}/2018/12/06/snow-winter` } + ].forEach(({ route, pathElement }) => expect(isRoutePartiallyMatch(route, pathElement)).toBeFalsy()); +});