import { format } from "date-fns"; import hljs from 'highlight.js'; function request(method, source, data = null) { return new Promise(function (resolve, reject){ let xhr = new XMLHttpRequest(); xhr.open(method, source, true); xhr.setRequestHeader("Content-Type", "application/json"); xhr.send(data); xhr.onload = function() { if(this.status >= 200 && this.status < 300){ resolve(xhr.response); } resolve({ status: this.status, statusText: xhr.statusText }); }; xhr.onerror = () => { resolve({ status: this.status, statusText: xhr.statusText }); } }); } /** * Create an HTML element * @param {String} tag * A HTML tag * * @param {String} id * An id * * @param {Array} class_list * An array of classes * * @param {Object} attributes * An object with attributes. * * @return {HTMLElement} * The resulting element * */ function createElement(tag, id, class_list, attributes) { const element = document.createElement(tag); if(id) { element.setAttribute("id", id); } if(class_list) { class_list.forEach(_class => { element.classList.add(_class); }); } if(attributes) { for(const [key, value] of Object.entries(attributes)) { element.setAttribute(key, value); } } return element; } async function buildHeader(container, endpoint, title_text, about_text, repo_page = false) { const info = JSON.parse(await request("GET", `http://localhost:1337/api/v1/${endpoint}`))["data"]; const row_div = createElement("div", null, ["row", "mx-0"]); const col_div = createElement("div", "header", ["col", "d-flex", "mt-3"], null); const title_div = createElement("div", null, ["d-inline"]); let title; switch(repo_page) { case true: title = createElement("span", "title", ["fs-1"]); col_div.classList.add("ms-2"); title_div.classList.add("ms-3"); const back_div = createElement("div", null, ["d-inline"]); const back_link = createElement("a", null, null, { "href": "/" }); const back = createBackButtonSVG(); back_link.appendChild(back); back_div.appendChild(back_link); col_div.appendChild(back_div); break; case false: title = createElement("a", "title", ["fs-1"], { "href": "/" }); col_div.classList.add("ms-4"); break; } title.appendChild(document.createTextNode(info[title_text])); const about = createElement("p", "about", ["mb-3", "fs-4"]); about.appendChild(document.createTextNode(info[about_text])); title_div.appendChild(title); title_div.appendChild(about); col_div.appendChild(title_div); row_div.appendChild(col_div); container.appendChild(row_div); } function buildProjectsHeader(container) { const row_div = createElement("div", null, ["row", "mx-0", "mt-5"]); // Title column const title_col_div = createElement("div", "projects-header", ["col", "ms-4"]); const projects_title = createElement("p", null, ["fs-1"]); projects_title.appendChild(document.createTextNode("Projects")); title_col_div.appendChild(projects_title); // Search column const search_col_div = createElement("div", "projects-search", ["col", "d-flex", "justify-content-end"]); const form = createElement("form"); const search = createElement("input", null, null, { "type": "search", "name": "q" }); const submit = createElement("input", null, null, { "type": "submit", "value": "Search" }); form.appendChild(search); form.appendChild(submit); search_col_div.appendChild(form); row_div.appendChild(title_col_div); row_div.appendChild(search_col_div); container.appendChild(row_div); } async function buildProjects(container) { const row_div = createElement("div", null, ["row", "mx-0"], null); const col_div = createElement("div", null, ["col", "ms-4"], null); const list = createElement("ul", "repos"); const repos = JSON.parse(await request("GET", "http://localhost:1337/api/v1/repos"))["data"]; const params = new URLSearchParams(window.location.search); const search = params.get("q"); for(const [key, value] of Object.entries(repos)) { const li = createElement("li"); const repo_div = createElement("div"); const repo_title = createElement("p", null, ["fs-3"]); const link = createElement("a", null, null, { "href": key }); link.appendChild(document.createTextNode(key)); repo_title.appendChild(link); const repo_last_updated = createElement("span", null, ["repo-last-updated", "fs-5"]); repo_last_updated.appendChild(document.createTextNode(`Last updated about ${value["last_updated"]} ago`)); const repo_desc = createElement("span", null, ["fs-5"]); repo_desc.appendChild(document.createTextNode(value["description"])); repo_div.appendChild(repo_title) repo_div.appendChild(repo_last_updated) repo_div.appendChild(repo_desc) li.appendChild(repo_div); if(search !== null) { if(key.indexOf(search) != -1) { list.appendChild(li); } } else { list.appendChild(li); } } col_div.appendChild(list); row_div.appendChild(col_div); container.appendChild(row_div); } function buildRepoNavbar(container, repo, page) { const row_div = createElement("div", "navbar", ["row", "mx-0"]); const col_div = createElement("div", "repo-navbar", ["col", "ms-4", "ps-4"]); const nav = createElement("nav", null, ["navbar", "navbar-expand", "navbar-dark"]); const nav_container = createElement("div", null, ["container-fluid", "px-0"]); const nav_collapse = createElement("div", null, ["collapse", "navbar-collapse"]); const nav_nav = createElement("ul", null, ["navbar-nav"]); const nav_items = ["log", "refs", "tree"]; nav_items.forEach(item => { const item_li = createElement("li", null, ["nav-item"]); const item_link = createElement("a", null, ["nav-link", "fs-4"], { "href": `/${repo}/${item}` }); if(item === page) { item_link.classList.add("active"); item_link.setAttribute("aria-current", "page"); } item_link.appendChild(document.createTextNode(item)); item_li.appendChild(item_link); nav_nav.appendChild(item_li); }); nav_collapse.appendChild(nav_nav); nav_container.appendChild(nav_collapse); nav.appendChild(nav_container); col_div.appendChild(nav); row_div.appendChild(col_div); container.appendChild(row_div); } async function buildLog(container, repo) { const row_div = createElement("div", null, ["row", "mx-0"], null); const col_div = createElement("div", null, ["col", "ms-4", "ps-4", "ps-sm-5", "mt-3"], null); const table = createElement("table", "log", ["table", "table-dark", "fs-5"]); const log = JSON.parse(await request("GET", `http://localhost:1337/api/v1/repos/${repo}/log`))["data"]; const thead = createElement("thead"); const header_tr = createElement("tr"); ["Subject", "Author", "Date", "Files", "Del/Add"].forEach(header => { const header_th = createElement("th", null, ["text-secondary"]); header_th.appendChild(document.createTextNode(header)); header_tr.appendChild(header_th); }); thead.appendChild(header_tr); table.appendChild(thead); const tbody = createElement("tbody"); log.forEach(commit => { const tr = createElement("tr"); const message = createElement("td"); const message_link = createElement("a", null, null, { "href": `log/${commit["commit"]}` }); message_link.appendChild(document.createTextNode(commit["message"])); message.appendChild(message_link); const author = createElement("td"); author.appendChild(document.createTextNode(commit["author_name"])); const date = createElement("td"); date.appendChild(document.createTextNode(format(new Date(commit["date"]), "yyyy-MM-dd hh:mm"))); const files_changed = createElement("td"); files_changed.appendChild(document.createTextNode(commit["files_changed"])); const del_add = createElement("td"); const deletions = createElement("span", null, ["text-danger"]) deletions.appendChild(document.createTextNode(`-${commit["deletions"]}`)); const insertions = createElement("span", null, ["text-success"]) insertions.appendChild(document.createTextNode(`+${commit["insertions"]}`)); del_add.appendChild(deletions); del_add.appendChild(document.createTextNode(" / ")) del_add.appendChild(insertions); tr.appendChild(message); tr.appendChild(author); tr.appendChild(date); tr.appendChild(files_changed); tr.appendChild(del_add) tbody.appendChild(tr); }); table.appendChild(tbody); col_div.appendChild(table); row_div.appendChild(col_div); container.appendChild(row_div); } const languages = [ { "name": "arduino", "extensions": [".ino"]}, { "name": "actionscript", "extensions": [".as"]}, { "name": "bash", "extensions": [".sh", ".zsh"]}, { "name": "csharp", "extensions": [".cs"]}, { "name": "c", "extensions": [".c", ".h"]}, { "name": "cpp", "extensions": [".cpp", ".hpp"]}, { "name": "cmake", "extensions": ["cmake.in"]}, { "name": "css", "extensions": [".css"]}, { "name": "d", "extensions": [".d"]}, { "name": "dos", "extensions": [".bat", ".cmd"]}, { "name": "dockerfile", "extensions": ["dockerfile", "Dockerfile"]}, { "name": "go", "extensions": [".go"]}, { "name": "gradle", "extensions": [".gradle"]}, { "name": "xml", "extensions": [".xml", ".html", ".xhtml", ".rss", ".atom", ".xjb", ".xsd", ".xsl", ".plist", ".svg"]}, { "name": "haskell", "extensions": [".hs"]}, { "name": "ini", "extensions": [".ini", ".toml"]}, { "name": "json", "extensions": [".json"]}, { "name": "java", "extensions": [".java", ".jsp"]}, { "name": "javascript", "extensions": [".js", ".jsx"]}, { "name": "kotlin", "extensions": [".kt"]}, { "name": "lua", "extensions": [".lua"]}, { "name": "makefile", "extensions": ["makefile", "Makefile"]}, { "name": "markdown", "extensions": [".md"]}, { "name": "objectivec", "extensions": [".m", ".mm", ".M"]}, { "name": "php", "extensions": [".php"]}, { "name": "perl", "extensions": [".pl", ".pm"]}, { "name": "plaintext", "extensions": [".txt"]}, { "name": "pgsql", "extensions": [".pgsql"]}, { "name": "powershell", "extensions": [".ps", ".ps1"]}, { "name": "python", "extensions": [".py"]}, { "name": "ruby", "extensions": [".rb"]}, { "name": "rust", "extensions": [".rs"]}, { "name": "scss", "extensions": [".scss"]}, { "name": "sql", "extensions": [".sql"]}, { "name": "swift", "extensions": [".swift"]}, { "name": "typescript", "extensions": [".ts"]}, { "name": "vbnet", "extensions": [".vb"]}, { "name": "vba", "extensions": [".vba"]}, { "name": "vbscript", "extensions": [".vbs"]}, { "name": "vim", "extensions": [".vim"]}, { "name": "yml", "extensions": [".yml"]} ]; async function buildCommit(container, repo, hash) { const row_div = createElement("div", null, ["row", "mx-0"], null); const col_div = createElement("div", null, ["col", "ms-2", "ps-4", "ps-sm-5", "fs-5"], null); const breadcrumb = createElement("nav", null, null, { "aria-label": "breadcrumb" }); const breadcrumb_ol = createElement("ol", null, ["breadcrumb"]); const breadcrumb_item_log = createElement("li", null, ["breadcrumb-item"], { "aria-current": "page" }); const breadcrumb_item_log_link = createElement("a", null, null, { "href": ".." }); breadcrumb_item_log_link.appendChild(document.createTextNode("Log")); breadcrumb_item_log.appendChild(breadcrumb_item_log_link); const breadcrumb_item_commit = createElement("li", null, ["breadcrumb-item", "active"], { "aria-current": "page" }); breadcrumb_item_commit.appendChild(document.createTextNode(hash)); breadcrumb_ol.appendChild(breadcrumb_item_log); breadcrumb_ol.appendChild(breadcrumb_item_commit); breadcrumb.appendChild(breadcrumb_ol); col_div.appendChild(breadcrumb); const commit = JSON.parse(await request("GET", `http://localhost:1337/api/v1/repos/${repo}/log/${hash}`)); const commit_info = createElement("table", "commit-info", ["table", "table-dark"]); const tbody = createElement("tbody"); ["author", "date", "message"].forEach((subject) => { const info = createElement("tr"); const title = createElement("td", null, ["commit-info-title"]); title.appendChild(document.createTextNode(subject.charAt(0).toUpperCase() + subject.slice(1))); const content = createElement("td", null); if(subject === "date") { content.appendChild(document.createTextNode(format(new Date(commit["data"]["date"]), "yyyy-MM-dd hh:mm"))); } else { content.appendChild(document.createTextNode(commit["data"][subject])); } info.appendChild(title); info.appendChild(content); tbody.appendChild(info); }); commit_info.appendChild(tbody); col_div.appendChild(commit_info); commit["data"]["patches"].forEach((patch) => { const file_div = createElement("div", null, ["commit-file"]); // Header const file_header = createElement("div", null, ["commit-file-header"]); const file_name = createElement("span", null, ["fw-bold"]); const file_deleted = createElement("span"); if(patch["to"] === "/dev/null") { file_name.appendChild(document.createTextNode(patch["from"])); file_deleted.appendChild(document.createTextNode("Deleted")); } else { file_name.appendChild(document.createTextNode(patch["to"])); file_deleted.appendChild(document.createTextNode("")); } file_header.appendChild(file_name); file_header.appendChild(file_deleted); const file_add_del = createElement("div", null, ["commit-file-add-del"]); const file_add = createElement("span"); const file_del = createElement("span"); file_add.appendChild(document.createTextNode(`+${patch["additions"]}`)); file_add_del.appendChild(file_add); file_del.appendChild(document.createTextNode(`-${patch["deletions"]}`)); file_add_del.appendChild(file_del); file_header.appendChild(file_add_del); file_div.appendChild(file_header); console.log(patch); // The diff if(patch["too_large"] === false) { let full_patch = ""; patch["hunks"].forEach((hunk) => { full_patch = `${full_patch}${hunk["hunk"]}\n`; }); const patch_table = createElement("table", null, null, { "cellspacing": "0px" }); const patch_tbody = createElement("tbody"); const language = languages.find((lang) => lang["extensions"].some((extension) => patch["to"].endsWith(extension))); const highlighted = language ? hljs.highlight(language["name"], full_patch) : hljs.highlightAuto(full_patch); const highlighted_patch = highlighted["value"].split('\n'); let index = 0; patch["hunks"].forEach((hunk) => { const hunk_length = hunk["hunk"].split('\n').length; const end = index + hunk_length; const unhighlighted_hunk = hunk["hunk"].split('\n'); hunk["hunk"] = highlighted_patch.slice(index, end); index = end; let new_offset = 0; let deleted_offset = 0; const multiline_tags = []; hunk["hunk"].forEach((line, line_index) => { //console.log(line_index + " " + line); const line_tr = createElement("tr"); const old_line_num = createElement("td"); const line_num = createElement("td"); const line_change = createElement("td"); const line_content = createElement("td"); const line_code = createElement("code"); if(/^@@\ -[0-9,]+\ \+[0-9,]+\ @@/.test(unhighlighted_hunk[line_index])) { line_tr.classList.add("commit-file-pos-change"); for(let i = 0; i < 3; i++) { const triple_dot = createElement("td"); triple_dot.appendChild(document.createTextNode("...")); line_tr.appendChild(triple_dot); } line_code.innerHTML = unhighlighted_hunk[line_index]; line_content.appendChild(line_code) line_tr.appendChild(line_content) new_offset++; deleted_offset++; } else if(/^\\\ No\ newline\ at\ end\ of\ file$/.test(unhighlighted_hunk[line_index])) { line_tr.classList.add("commit-file-no-newline"); for(let i = 0; i < 3; i++) { const empty = createElement("td"); empty.appendChild(document.createTextNode("")); line_tr.appendChild(empty); } line_code.innerHTML = unhighlighted_hunk[line_index]; line_content.appendChild(line_code) line_tr.appendChild(line_content) new_offset++; deleted_offset++; } else { if(hunk["new"].includes(line_index)) { deleted_offset++; line_num.appendChild(document.createTextNode(Number(hunk["new_start"]) + line_index - new_offset)); line_num.classList.add("line-highlight-new"); line_change.appendChild(document.createTextNode("+")); line_change.classList.add("line-new"); } else if(hunk["deleted"].includes(line_index)) { new_offset++; old_line_num.appendChild(document.createTextNode(Number(hunk["old_start"]) + line_index - deleted_offset)); line_num.classList.add("line-highlight-deleted"); line_change.appendChild(document.createTextNode("-")); line_change.classList.add("line-deleted"); } else { old_line_num.appendChild(document.createTextNode(Number(hunk["old_start"]) + line_index - deleted_offset)); old_line_num.classList.add("line-unchanged"); line_num.appendChild(document.createTextNode(Number(hunk["new_start"]) + line_index - new_offset)); line_num.classList.add("line-unchanged"); line_change.appendChild(document.createTextNode(" ")); } line_tr.appendChild(old_line_num); line_tr.appendChild(line_num); line_tr.appendChild(line_change); let comment_open = line.match(//g); const comment_open_cnt = (comment_open !== null) ? comment_open.length : 0; comment_open = (comment_open !== null) ? comment_open[0] : ""; let comment_close = line.match(/<\/span>/g); const comment_close_cnt = (comment_close !== null) ? comment_close.length : 0; comment_close = (comment_close !== null) ? comment_close[0] : ""; if(comment_open_cnt > comment_close_cnt) { line_code.innerHTML = line + ""; console.log("Öppning " + line); multiline_tags.push(comment_open); } else if(comment_open_cnt < comment_close_cnt && multiline_tags.length !== 0) { line_code.innerHTML = multiline_tags[multiline_tags.length - 1] + line; console.log("Stängning " + line + " " + multiline_tags[multiline_tags.length - 1]); multiline_tags.pop(); } else if(multiline_tags.length !== 0) { line_code.innerHTML = multiline_tags[multiline_tags.length - 1] + line + ""; console.log("Mitt i " + line); } else { line_code.innerHTML = line; } line_content.appendChild(line_code); line_tr.appendChild(line_content) } patch_tbody.appendChild(line_tr); }); }) patch_table.appendChild(patch_tbody); file_div.appendChild(patch_table); } else { const patch_too_large_div = createElement("div", null, ["ps-3", "pt-3", "patch-too-large"]); const patch_too_large = createElement("span"); patch_too_large.appendChild(document.createTextNode("Patch is too large to display.")); patch_too_large_div.appendChild(patch_too_large); file_div.appendChild(patch_too_large_div); } col_div.appendChild(file_div); }); row_div.appendChild(col_div); container.appendChild(row_div); } function createBackButtonSVG() { const xmlns = "http://www.w3.org/2000/svg"; let svg = document.createElementNS(xmlns, "svg"); svg.setAttributeNS(null, "id", "back"); svg.setAttributeNS(null, "height", "24px"); svg.setAttributeNS(null, "width", "24px"); svg.setAttributeNS(null, "viewBox", "0 0 24 24"); svg.setAttributeNS(null, "fill", "#FFFFFF"); const path_one = document.createElementNS(xmlns, "path"); path_one.setAttributeNS(null, "d", "M0 0h24v24H0z"); path_one.setAttributeNS(null, "fill", "none"); const path_two = document.createElementNS(xmlns, "path"); path_two.setAttributeNS(null, "d", "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"); svg.appendChild(path_one); svg.appendChild(path_two); return svg; } document.addEventListener("DOMContentLoaded", async function () { let path = window.location.pathname; const container = document.getElementById("container"); if(path === "/") { await buildHeader(container, "info", "title", "about"); buildProjectsHeader(container); buildProjects(container); return } const path_valid_and_split = /^\/([a-zA-Z0-9\.\-_]+)\/([a-z]+)(?:\/([0-9a-f]+))?$/; if(path_valid_and_split.test(path)) { path = path_valid_and_split.exec(path); const repo = path[1]; const page = path[2]; const sub_page = path[3]; console.log("Tjena!"); if(page === "log") { await buildHeader(container, `repos/${repo}`, "name", "description", true); buildRepoNavbar(container, repo, page); if(sub_page) { buildCommit(container, repo, sub_page); return; } buildLog(container, repo); } } });