diff2html/website/templates/pages/demo/demo.ts
2019-12-22 18:00:52 +00:00

287 lines
10 KiB
TypeScript

import { Diff2HtmlUI, defaultDiff2HtmlUIConfig, Diff2HtmlUIConfig } from "../../../../src/ui/js/diff2html-ui";
import "../../../main.ts";
import "../../../main.css";
import "./demo.css";
import "../../../../src/ui/css/diff2html.css";
/*
* 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;
[key: string]: string | boolean | number | 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[0-1])(?:\.\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]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[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 {
console.info("Could not parse url, using the provided url.");
fetchUrl = "https://crossorigin.me/" + url;
}
return {
url: fetchUrl,
headers: headers
};
}
function getConfiguration(urlParams: URLParams): Diff2HtmlUIConfig {
// Removing `diff` form `urlParams` to avoid being inserted
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { diff, ...urlParamsRest } = urlParams;
const config: URLParams = {
...defaultDiff2HtmlUIConfig,
...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<string> {
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(diffString, elements.structure.diffTarget, config);
if (config.outputFormat === "side-by-side") {
elements.structure.container.style.width = "100%";
} else {
elements.structure.container.style.width = "";
}
diff2htmlUi.draw();
}
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 {
if (history.pushState) {
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: {
container: HTMLElement;
diffTarget: HTMLElement;
};
url: {
input: HTMLInputElement;
button: HTMLElement;
};
options: {
outputFormat: HTMLInputElement;
matching: HTMLInputElement;
wordsThreshold: HTMLInputElement;
matchingMaxComparisons: HTMLInputElement;
};
checkboxes: {
drawFileList: HTMLInputElement;
};
};
document.addEventListener("DOMContentLoaded", async () => {
// Improves browser compatibility
require("whatwg-fetch");
const drawAndUpdateUrl = async (
diffUrl: string,
diffString: string,
config: Diff2HtmlUIConfig,
elements: Elements
): Promise<void> => {
updateBrowserUrl(config, diffUrl);
const newRequest = prepareRequest(diffUrl);
diffString = await getDiff(newRequest);
draw(diffString, config, elements);
};
const elements: Elements = {
structure: {
container: document.getElementsByClassName("container")[0] as HTMLElement,
diffTarget: document.getElementById("url-diff-container") as HTMLElement
},
url: {
input: document.getElementById("url") as HTMLInputElement,
button: document.getElementById("url-btn") as HTMLElement
},
options: {
outputFormat: document.getElementById("diff-url-options-output-format") as HTMLInputElement,
matching: document.getElementById("diff-url-options-matching") as HTMLInputElement,
wordsThreshold: document.getElementById("diff-url-options-match-words-threshold") as HTMLInputElement,
matchingMaxComparisons: document.getElementById("diff-url-options-matching-max-comparisons") as HTMLInputElement
},
checkboxes: {
drawFileList: document.getElementById("diff-url-options-show-files") as HTMLInputElement
}
};
let [config, diffString] = await prepareInitialState(elements);
// Update HTML inputs from any changes in URL
config.outputFormat && (elements.options.outputFormat.value = config.outputFormat);
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);
});