diff --git a/src/web/src/components/GithubInfoProvider.tsx b/src/web/src/components/GithubInfoProvider.tsx new file mode 100644 index 0000000000..7f97dd45ba --- /dev/null +++ b/src/web/src/components/GithubInfoProvider.tsx @@ -0,0 +1,133 @@ +import { createContext, ReactNode, useMemo } from 'react'; +import githubReservedNames from '../githubReservedName'; + +export interface GithubInfoContextInterface { + issues: string[]; + pullRequests: string[]; + repos: string[]; + commits: string[]; + users: string[]; +} + +const GithubInfoContext = createContext({ + issues: [], + pullRequests: [], + repos: [], + commits: [], + users: [], +}); + +type Props = { + children: ReactNode; + htmlString?: string; +}; + +const extractGitHubUrlsFromPost = (htmlString: string): string[] => { + const parser = new DOMParser(); + const postDoc = parser.parseFromString(htmlString, 'text/html'); + + const allGithubLinks = Array.from( + // all links that have href starts with 'https://github.com' + postDoc.querySelectorAll("a[href^='https://github.com']"), + (element) => (element as HTMLAnchorElement).href + ); + + // unique links only + return allGithubLinks.reduce( + (acc: string[], element) => (acc.includes(element) ? acc : [...acc, element]), + [] + ); +}; + +const GithubInfoProvider = ({ children, htmlString }: Props) => { + const extractedGitHubUrls: string[] = useMemo( + () => (htmlString ? extractGitHubUrlsFromPost(htmlString) : []), + [htmlString] + ); + + const parseGitHubUrl = (url: string): URL | null => { + try { + const ghUrl = new URL(url); + if (ghUrl.hostname !== 'github.com') { + return null; + } + return ghUrl; + } catch (err) { + return null; + } + }; + + const filterGitHubUrls = () => { + const issues: Set = new Set(); + const pullRequests: Set = new Set(); + const repos: Set = new Set(); + const commits: Set = new Set(); + const users: Set = new Set(); + + const ghUrls = ( + extractedGitHubUrls.map((url) => parseGitHubUrl(url)).filter((url) => url !== null) as URL[] + ).filter((url) => !githubReservedNames.includes(url.pathname.split('/').slice(1, 2)[0])); + + ghUrls.forEach((url) => { + const { pathname } = url; + + // Match urls that start with / and optionally end with / or //// + // can be number, or a mixed of 40 alphanumeric (commit id) + // Ex: /Seneca-CDOT/telescope/pull/2367 ✅ + // Ex: /Seneca-CDOT/telescope ✅ + // Ex: /Seneca-CDOT/telescope/pull/2367/commits/d3fagd3fagd3fagd3fagd3fagd3fag4d41265748 ✅ + // Ex: /Seneca-CDOT/telescope/issues ✅ + const matches = + /^\/(?[^/]+)(\/(?[^/]+)((\/(.*))?(\/(?[^/]+)?\/(?(\d+|\w{40}))\/?$))?)?/gi.exec( + pathname + ); + + if (matches?.groups) { + const { type, user, repo } = matches.groups; + + // if repo defined add to repos + if (repo) { + const repoUrl = `${user}/${repo}`; + repos.add(repoUrl); + } + users.add(user); + switch (type?.toLowerCase()) { + case 'pull': + pullRequests.add(pathname); + break; + + case 'issues': + issues.add(pathname); + break; + + case 'commit': + case 'commits': + commits.add(pathname); + break; + + default: + break; + } + } + }); + + return { + repos: Array.from(repos), + issues: Array.from(issues), + pullRequests: Array.from(pullRequests), + commits: Array.from(commits), + users: Array.from(users), + }; + }; + + const { repos, issues, commits, users, pullRequests } = filterGitHubUrls(); + + return ( + + {children} + + ); +}; + +export default GithubInfoProvider; +export { GithubInfoContext }; diff --git a/src/web/src/components/Posts/Commits.tsx b/src/web/src/components/Posts/Commits.tsx index e2b21b0168..14018831fd 100644 --- a/src/web/src/components/Posts/Commits.tsx +++ b/src/web/src/components/Posts/Commits.tsx @@ -1,5 +1,6 @@ import { VscGitCommit } from 'react-icons/vsc'; import { createStyles, makeStyles, Theme } from '@material-ui/core'; +import useGithubInfo from '../../hooks/use-githubInfo'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -46,21 +47,18 @@ const getCommitInfo = (commit: string) => { return `${user}/${repo}`; }; -type Props = { - commitUrls: string[]; -}; - -const Commits = ({ commitUrls }: Props) => { +const Commits = () => { const classes = useStyles(); + const { commits } = useGithubInfo(); return (

- {commitUrls.length === 1 ? 'Commit' : 'Commits'} + {commits.length === 1 ? 'Commit' : 'Commits'}