From 713f9c1629488807ebb3b250dbf9f0c3933c18e5 Mon Sep 17 00:00:00 2001 From: Khinshan Khan Date: Sat, 25 Jan 2020 15:34:34 -0500 Subject: [PATCH] refact(404): abstract 404 link correction logic for collections to a component --- src/components/404LinkCorrection/index.tsx | 108 +++++++++++++++++++++ src/pages/resources/404.tsx | 108 +-------------------- 2 files changed, 111 insertions(+), 105 deletions(-) create mode 100644 src/components/404LinkCorrection/index.tsx diff --git a/src/components/404LinkCorrection/index.tsx b/src/components/404LinkCorrection/index.tsx new file mode 100644 index 0000000..7d3339e --- /dev/null +++ b/src/components/404LinkCorrection/index.tsx @@ -0,0 +1,108 @@ +import { WindowLocation } from "@reach/router" +import { Link } from "gatsby" +import React, { Fragment } from "react" + +import { FileConnection } from "../../../generated/graphql" + +// helper function to make matrix generation easier +// credits to https://stackoverflow.com/a/13808461 +function makeMatrix(w: number, h: number, val: any = null) { + return Array(h) + .fill(null) + .map(() => Array(w).fill(val)) +} + +// calculate semantic difference +// based on one character edits +// efficient refactoring inspired by MIT licensed repo: +// https://github.com/trekhleb/javascript-algorithms +function levenshteinDistance(term1: string, term2: string) { + /* base case: empty strings */ + if (term1.length === 0) { + return term2.length + } + if (term2.length === 0) { + return term1.length + } + + // this will be our comparison matrix + const matrix = makeMatrix(term1.length + 1, term2.length + 1, null) + + // make zeroeth row based off first term + for (let i = 0; i <= term1.length; i += 1) { + matrix[0][i] = i + } + + // make zeroeth column based off second term + for (let j = 0; j <= term2.length; j += 1) { + matrix[j][0] = j + } + + // iterative with full matrix implementation + // https://en.wikipedia.org/wiki/Levenshtein_distance + for (let j = 1; j <= term2.length; j += 1) { + for (let i = 1; i <= term1.length; i += 1) { + const substitutionCost = term1[i - 1] === term2[j - 1] ? 0 : 1 + + matrix[j][i] = Math.min( + matrix[j - 1][i] + 1, // deletion + matrix[j][i - 1] + 1, // insertion + matrix[j - 1][i - 1] + substitutionCost + ) // substitution + } + } + + return matrix[term2.length][term1.length] +} + +export const getPossibleCorrections = ( + basepath: string, + location: WindowLocation, + data: { allFile: FileConnection } +) => { + // the data prop has the graphql result + // we're abstracting it to `linkArray` to just have array of edges matched + // must use `.node.absolutePath` on each edge to get each node's absolute link + // we will compare this link + // however, the relative links are more user friendly + const linkArray = data.allFile.edges + + // location prop has several attributes + // location.origin is base url + // location.href is the window location + // location.pathname is link after protocol and hostname + // by replacing `"/resources/"`, + // we are left with only the relative path to `resources` + const search = location.pathname.replace(`/${basepath}/`, "") + + // filter based on distance + // levenshtein distance of x means how 'off' it was + const displayArray = linkArray.filter( + value => + levenshteinDistance( + search.toLowerCase(), + value.node!.relativePath!.toLowerCase() + ) < 8 + ) + + // helpful message if no matches found + const found = displayArray.length === 0 ? "Oops, nothing similar found." : "" + + return ( + +

Based off of "{search}" you may have meant:

+ + {found} +
+ ) +} diff --git a/src/pages/resources/404.tsx b/src/pages/resources/404.tsx index aaa6267..bd3990f 100644 --- a/src/pages/resources/404.tsx +++ b/src/pages/resources/404.tsx @@ -1,114 +1,12 @@ -import { Location, WindowLocation } from "@reach/router" -import { graphql, Link } from "gatsby" +import { Location } from "@reach/router" +import { graphql } from "gatsby" import React, { Fragment } from "react" import { FileConnection } from "../../../generated/graphql" import { ComponentQuery } from "../../../typings" +import { getPossibleCorrections } from "../../components/404LinkCorrection" import { SEO } from "../../components/SEO" -// helper function to make matrix generation easier -// credits to https://stackoverflow.com/a/13808461 -function makeMatrix(w: number, h: number, val: any = null) { - return Array(h) - .fill(null) - .map(() => Array(w).fill(val)) -} - -// calculate semantic difference -// based on one character edits -// efficient refactoring inspired by MIT licensed repo: -// https://github.com/trekhleb/javascript-algorithms -function levenshteinDistance(term1: string, term2: string) { - /* base case: empty strings */ - if (term1.length === 0) { - return term2.length - } - if (term2.length === 0) { - return term1.length - } - - // this will be our comparison matrix - const matrix = makeMatrix(term1.length + 1, term2.length + 1, null) - - // make zeroeth row based off first term - for (let i = 0; i <= term1.length; i += 1) { - matrix[0][i] = i - } - - // make zeroeth column based off second term - for (let j = 0; j <= term2.length; j += 1) { - matrix[j][0] = j - } - - // iterative with full matrix implementation - // https://en.wikipedia.org/wiki/Levenshtein_distance - for (let j = 1; j <= term2.length; j += 1) { - for (let i = 1; i <= term1.length; i += 1) { - const substitutionCost = term1[i - 1] === term2[j - 1] ? 0 : 1 - - matrix[j][i] = Math.min( - matrix[j - 1][i] + 1, // deletion - matrix[j][i - 1] + 1, // insertion - matrix[j - 1][i - 1] + substitutionCost - ) // substitution - } - } - - return matrix[term2.length][term1.length] -} - -function getPossibleCorrections( - basepath: string, - location: WindowLocation, - data: { allFile: FileConnection } -) { - // the data prop has the graphql result - // we're abstracting it to `linkArray` to just have array of edges matched - // must use `.node.absolutePath` on each edge to get each node's absolute link - // we will compare this link - // however, the relative links are more user friendly - const linkArray = data.allFile.edges - - // location prop has several attributes - // location.origin is base url - // location.href is the window location - // location.pathname is link after protocol and hostname - // by replacing `"/resources/"`, - // we are left with only the relative path to `resources` - const search = location.pathname.replace(`/${basepath}/`, "") - - // filter based on distance - // levenshtein distance of x means how 'off' it was - const displayArray = linkArray.filter( - value => - levenshteinDistance( - search.toLowerCase(), - value.node!.relativePath!.toLowerCase() - ) < 8 - ) - - // helpful message if no matches found - const found = displayArray.length === 0 ? "Oops, nothing similar found." : "" - - return ( - -

Based off of "{search}" you may have meant:

- - {found} -
- ) -} - export default ({ data }: ComponentQuery<{ allFile: FileConnection }>) => (