import { Diff2HtmlUI, defaultDiff2HtmlUIConfig, Diff2HtmlUIConfig } from '../../../../src/ui/js/diff2html-ui-slim'; import '../../../main.ts'; import '../../../main.css'; import './github-highlights.css'; import '../../../../src/ui/css/diff2html.css'; import './demo.css'; import { colorSchemeToCss } from '../../../../src/render-utils'; import { ColorSchemeType } from '../../../../src/types'; /* * Example URLs: * * https://github.com/rtfpessoa/diff2html/commit/7d02e67f3b3386ac5d804f974d025cd7a1165839 * https://github.com/rtfpessoa/diff2html/pull/106 * * https://gitlab.com/gitlab-org/gitlab-ce/commit/4e963fed42ad518caa7353d361a38a1250c99c41 * https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6763 * * https://bitbucket.org/atlassian/amps/commits/52c38116f12475f75af4a147b7a7685478b83eca * https://bitbucket.org/atlassian/amps/pull-requests/236 */ type URLParams = { diff?: string; diffTooBigMessage?: string; [key: string]: string | boolean | number | Map | undefined; }; const searchParam = 'diff'; function getParamsFromSearch(search: string): URLParams { try { return search .split('?')[1] .split('&') .reduce((urlParams, e) => { const values = e.split('='); return { ...urlParams, [values[0]]: values[1], }; }, {}); } catch (_ignore) { return {}; } } function validateUrl(url: string): boolean { return /^(?:(?:https?|ftp):)?\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4])|(?:[\da-z\u00a1-\uffff]-*)*[\da-z\u00a1-\uffff]+(?:\.(?:[\da-z\u00a1-\uffff]-*)*[\da-z\u00a1-\uffff]+)*\.[a-z\u00a1-\uffff]{2,}.?)(?::\d{2,5})?(?:[#/?]\S*)?$/i.test( url, ); } type Request = { url: string; headers: Headers; }; function prepareRequest(url: string): Request { if (!validateUrl(url)) { const errorMsg = 'Invalid url provided!'; console.error(errorMsg); throw new Error(errorMsg); } let fetchUrl; const headers = new Headers(); const githubCommitUrl = /^https?:\/\/(?:www\.)?github\.com\/(.*?)\/(.*?)\/commit\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; const githubPrUrl = /^https?:\/\/(?:www\.)?github\.com\/(.*?)\/(.*?)\/pull\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; const gitlabCommitUrl = /^https?:\/\/(?:www\.)?gitlab\.com\/(.*?)\/(.*?)\/commit\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; const gitlabPrUrl = /^https?:\/\/(?:www\.)?gitlab\.com\/(.*?)\/(.*?)\/merge_requests\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; const bitbucketCommitUrl = /^https?:\/\/(?:www\.)?bitbucket\.org\/(.*?)\/(.*?)\/commits\/(.*?)(?:\/raw)?(?:\/.*)?$/; const bitbucketPrUrl = /^https?:\/\/(?:www\.)?bitbucket\.org\/(.*?)\/(.*?)\/pull-requests\/(.*?)(?:\/.*)?$/; function gitLabUrlGen(userName: string, projectName: string, type: string, value: string): string { return ( 'https://crossorigin.me/https://gitlab.com/' + userName + '/' + projectName + '/' + type + '/' + value + '.diff' ); } function gitHubUrlGen(userName: string, projectName: string, type: string, value: string): string { headers.append('Accept', 'application/vnd.github.v3.diff'); return 'https://api.github.com/repos/' + userName + '/' + projectName + '/' + type + '/' + value; } function bitbucketUrlGen(userName: string, projectName: string, type: string, value: string): string { const baseUrl = 'https://bitbucket.org/api/2.0/repositories/'; if (type === 'pullrequests') { return baseUrl + userName + '/' + projectName + '/pullrequests/' + value + '/diff'; } return baseUrl + userName + '/' + projectName + '/diff/' + value; } let values; if ((values = githubCommitUrl.exec(url))) { fetchUrl = gitHubUrlGen(values[1], values[2], 'commits', values[3]); } else if ((values = githubPrUrl.exec(url))) { fetchUrl = gitHubUrlGen(values[1], values[2], 'pulls', values[3]); } else if ((values = gitlabCommitUrl.exec(url))) { fetchUrl = gitLabUrlGen(values[1], values[2], 'commit', values[3]); } else if ((values = gitlabPrUrl.exec(url))) { fetchUrl = gitLabUrlGen(values[1], values[2], 'merge_requests', values[3]); } else if ((values = bitbucketCommitUrl.exec(url))) { fetchUrl = bitbucketUrlGen(values[1], values[2], 'commit', values[3]); } else if ((values = bitbucketPrUrl.exec(url))) { fetchUrl = bitbucketUrlGen(values[1], values[2], 'pullrequests', values[3]); } else { fetchUrl = url; } return { url: fetchUrl, headers: headers, }; } function getConfiguration(urlParams: URLParams): Diff2HtmlUIConfig { // Removing `diff` and `diffTooBigMessage` form `urlParams` to avoid being inserted // eslint-disable-next-line @typescript-eslint/no-unused-vars const { diff, diffTooBigMessage, ...urlParamsRest } = urlParams; const defaultColorScheme: ColorSchemeType = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? ColorSchemeType.DARK : ColorSchemeType.LIGHT; const config: URLParams = { ...defaultDiff2HtmlUIConfig, colorScheme: defaultColorScheme, ...urlParamsRest, }; return Object.entries(config).reduce((object, [k, v]) => { const newObject = !Number.isNaN(Number(v)) ? { [k]: Number(v) } : v === 'true' || v === 'false' ? { [k]: Boolean(v) } : { [k]: v }; return { ...object, ...newObject }; }, {}); } async function getDiff(request: Request): Promise { try { const result = await fetch(request.url, { method: 'GET', headers: request.headers, mode: 'cors', cache: 'default', }); return result.text(); } catch (error) { console.error('Failed to retrieve diff', error); throw error; } } function draw(diffString: string, config: Diff2HtmlUIConfig, elements: Elements): void { const diff2htmlUi = new Diff2HtmlUI(elements.structure.diffTarget, diffString, config); setBodyColorScheme(diff2htmlUi.config.colorScheme); diff2htmlUi.draw(); } function setBodyColorScheme(colorScheme: ColorSchemeType): void { document.body.className = colorSchemeToCss(colorScheme); } async function prepareInitialState(elements: Elements): Promise<[Diff2HtmlUIConfig, string]> { const urlParams = getParamsFromSearch(window.location.search); const currentUrl = (urlParams && urlParams[searchParam]) || 'https://github.com/rtfpessoa/diff2html/pull/106'; if (currentUrl !== elements.url.input.value) elements.url.input.value = currentUrl; const request = prepareRequest(currentUrl); const initialConfiguration = getConfiguration(urlParams); const initialDiff = await getDiff(request); return [initialConfiguration, initialDiff]; } function updateBrowserUrl(config: Diff2HtmlUIConfig, newDiffUrl: string): void { const paramString = Object.entries(config) .map(([k, v]) => k + '=' + v) .join('&'); const newPageUrl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + paramString + '&' + searchParam + '=' + newDiffUrl; window.history.pushState({ path: newPageUrl }, '', newPageUrl); } type Elements = { structure: { diffTarget: HTMLElement; }; url: { input: HTMLInputElement; button: HTMLElement; }; options: { outputFormat: HTMLInputElement; colorScheme: HTMLInputElement; matching: HTMLInputElement; wordsThreshold: HTMLInputElement; matchingMaxComparisons: HTMLInputElement; }; checkboxes: { drawFileList: HTMLInputElement; }; }; function isHTMLInputElement(arg?: unknown): arg is HTMLInputElement { return arg !== null && (arg as HTMLInputElement)?.value !== undefined; } function getHTMLInputElementById(id: string): HTMLInputElement { const element = document.getElementById(id); if (!isHTMLInputElement(element)) { throw new Error(`Could not find html input element with id '${id}'`); } return element; } function getHTMLElementById(id: string): HTMLElement { const element = document.getElementById(id); if (element === null) { throw new Error(`Could not find html element with id '${id}'`); } return element; } document.addEventListener('DOMContentLoaded', async () => { // Improves browser compatibility require('whatwg-fetch'); const drawAndUpdateUrl = async ( diffUrl: string, diffString: string, config: Diff2HtmlUIConfig, elements: Elements, ): Promise => { updateBrowserUrl(config, diffUrl); const newRequest = prepareRequest(diffUrl); diffString = await getDiff(newRequest); draw(diffString, config, elements); }; const elements: Elements = { structure: { diffTarget: getHTMLElementById('url-diff-container'), }, url: { input: getHTMLInputElementById('url'), button: getHTMLElementById('url-btn'), }, options: { outputFormat: getHTMLInputElementById('diff-url-options-output-format'), colorScheme: getHTMLInputElementById('diff-url-options-color-scheme'), matching: getHTMLInputElementById('diff-url-options-matching'), wordsThreshold: getHTMLInputElementById('diff-url-options-match-words-threshold'), matchingMaxComparisons: getHTMLInputElementById('diff-url-options-matching-max-comparisons'), }, checkboxes: { drawFileList: getHTMLInputElementById('diff-url-options-show-files'), }, }; let [config, diffString] = await prepareInitialState(elements); // Update HTML inputs from any changes in URL config.outputFormat && (elements.options.outputFormat.value = config.outputFormat); config.colorScheme && (elements.options.colorScheme.value = config.colorScheme); config.drawFileList && (elements.checkboxes.drawFileList.checked = config.drawFileList); config.matching && (elements.options.matching.value = config.matching); config.matchWordsThreshold && (elements.options.wordsThreshold.value = config.matchWordsThreshold.toString()); config.matchingMaxComparisons && (elements.options.matchingMaxComparisons.value = config.matchingMaxComparisons.toString()); Object.entries(elements.options).forEach(([option, element]) => element.addEventListener('change', () => { config = { ...config, [option]: element.value }; drawAndUpdateUrl(elements.url.input.value, diffString, config, elements); }), ); Object.entries(elements.checkboxes).forEach(([option, checkbox]) => checkbox.addEventListener('change', () => { config = { ...config, [option]: checkbox.checked }; drawAndUpdateUrl(elements.url.input.value, diffString, config, elements); }), ); elements.url.button.addEventListener('click', async e => { e.preventDefault(); const newDiffUrl = elements.url.input.value; const newRequest = prepareRequest(newDiffUrl); diffString = await getDiff(newRequest); drawAndUpdateUrl(newDiffUrl, diffString, config, elements); }); return drawAndUpdateUrl(elements.url.input.value, diffString, config, elements); });