/* cgit.js: javacript functions for cgit * * Copyright (C) 2006-2018 cgit Development Team * * Licensed under GNU General Public License v2 * (see LICENSE.txt for full license text) */ (function () { /* This follows the logic and suffixes used in ui-shared.c */ var age_classes = [ "age-mins", "age-hours", "age-days", "age-weeks", "age-months", "age-years" ]; var age_suffix = [ "min.", "hours", "days", "weeks", "months", "years", "years" ]; var age_next = [ 60, 3600, 24 * 3600, 7 * 24 * 3600, 30 * 24 * 3600, 365 * 24 * 3600, 365 * 24 * 3600 ]; var age_limit = [ 7200, 24 * 7200, 7 * 24 * 7200, 30 * 24 * 7200, 365 * 25 * 7200, 365 * 25 * 7200 ]; var update_next = [ 10, 5 * 60, 1800, 24 * 3600, 24 * 3600, 24 * 3600, 24 * 3600 ]; function render_age(e, age) { var t, n; for (n = 0; n < age_classes.length; n++) if (age < age_limit[n]) break; t = Math.round(age / age_next[n]) + " " + age_suffix[n]; if (e.textContent != t) { e.textContent = t; if (n == age_classes.length) n--; if (e.className != age_classes[n]) e.className = age_classes[n]; } } function aging() { var n, next = 24 * 3600, now_ut = Math.round((new Date().getTime() / 1000)); for (n = 0; n < age_classes.length; n++) { var m, elems = document.getElementsByClassName(age_classes[n]); if (elems.length && update_next[n] < next) next = update_next[n]; for (m = 0; m < elems.length; m++) { var age = now_ut - elems[m].getAttribute("data-ut"); /* A commit dated ahead of the viewer's clock would * otherwise render as a negative age; ui-shared.c * clamps the same way. */ if (age < 0) age = 0; render_age(elems[m], age); } } /* * We only need to come back when the age might have changed. * Eg, if everything is counted in hours already, once per * 5 minutes is accurate enough. */ window.setTimeout(aging, next * 1000); } document.addEventListener("DOMContentLoaded", function() { /* we can do the aging on DOM content load since no layout dependency */ aging(); }, false); })(); /* Selects marked data-autosubmit reload the page with the new setting. * Wired here instead of inline onchange handlers, which a strict * Content-Security-Policy blocks. Without scripting the forms keep * their noscript reload button. */ (function () { document.addEventListener("DOMContentLoaded", function () { var i, els = document.querySelectorAll("select[data-autosubmit]"); for (i = 0; i < els.length; i++) els[i].addEventListener("change", function () { this.form.submit(); }); }, false); })(); /* Wash a faded highlight over the source line targeted by the URL * fragment, #n231, or over a range like #n5-n12. The line anchors live * in the number gutter, so this measures the anchor and lays a * full-width bar across the code at the same height. Without JS the * CSS :target rule still tints the line number itself. */ (function () { var bar = null; function place() { var m, a, b, table, box, ra, rb, top, bottom; if (bar) { bar.remove(); bar = null; } m = /^#n(\d+)(?:-n?(\d+))?$/.exec(location.hash); if (!m) return; a = document.getElementById("n" + m[1]); if (!a) return; b = (m[2] && document.getElementById("n" + m[2])) || a; table = a.closest("table"); if (!table) return; box = table.getBoundingClientRect(); ra = a.getBoundingClientRect(); rb = b.getBoundingClientRect(); top = Math.min(ra.top, rb.top) - box.top; bottom = Math.max(ra.bottom, rb.bottom) - box.top; bar = document.createElement("div"); bar.className = "line-hl"; bar.style.top = top + "px"; bar.style.height = (bottom - top) + "px"; table.appendChild(bar); /* The browser only scrolls to fragments that name a real id, so * bring ranges into view ourselves. */ if (m[2]) a.scrollIntoView({ block: "center" }); } document.addEventListener("DOMContentLoaded", place, false); window.addEventListener("hashchange", place, false); })(); /* Colour theme toggle: cycles auto -> light -> dark and remembers the * choice. "auto" leaves the page following the system preference via CSS. */ (function () { var KEY = "cgit-theme"; var ORDER = [ "auto", "light", "dark" ]; var ICONS = { auto: '', light: '', dark: '' }; function saved() { try { return localStorage.getItem(KEY); } catch (e) { return null; } } function persist(value) { try { localStorage.setItem(KEY, value); } catch (e) { } } function apply(theme, root, btn) { if (theme === "auto") root.removeAttribute("data-theme"); else root.setAttribute("data-theme", theme); /* Keep the standalone page background in sync with the choice. */ document.documentElement.style.colorScheme = (theme === "auto") ? "" : theme; if (btn) { btn.innerHTML = ICONS[theme]; btn.title = "Colour theme: " + theme; } } document.addEventListener("DOMContentLoaded", function () { var root = document.getElementById("cgit"); if (!root) return; var btn = root.querySelector(".theme-toggle"); var theme = saved(); if (ORDER.indexOf(theme) < 0) theme = "auto"; apply(theme, root, btn); if (!btn) return; btn.hidden = false; btn.addEventListener("click", function () { theme = ORDER[(ORDER.indexOf(theme) + 1) % ORDER.length]; persist(theme); apply(theme, root, btn); }); }, false); })(); /* Built-in Markdown rendering for the about page. When no about-filter is * configured, cgit escapes a markdown readme into a data-markdown container * (see cgit_print_repo_readme) and this renders a deliberately small, safe * subset client-side: headings, lists, blockquotes, rules, fenced and inline * code, pipe tables, links, images and emphasis. Every run of text is escaped * before any markup is added, and link and image URLs are restricted to http, * https, mailto and relative targets, so a hostile readme cannot inject markup * or scripts. Fenced code carries its language in data-lang as a styling * hook. Without JavaScript the escaped source stays readable as plain text. * * This is intentionally a subset, not CommonMark: no reference links, raw HTML * passthrough, nested lists or setext headings. Configure an about-filter to * replace it, or set enable-markdown=0 to turn it off. */ (function () { var MAX_BYTES = 400000; function esc(s) { return s.replace(/&/g, "&").replace(//g, ">"); } function escAttr(s) { return esc(s).replace(/"/g, """).replace(/'/g, "'"); } /* Return the url if its scheme is safe, else "". Whitespace and control bytes * are stripped before the scheme is read because browsers ignore them when * resolving it, so "java\nscript:..." must still be caught as javascript. */ function safeUrl(url) { url = (url || "").replace(/[\u0000-\u0020]+/g, ""); var scheme = /^([a-z][a-z0-9+.\-]*):/i.exec(url); if (scheme && !/^(https?|mailto)$/i.test(scheme[1])) return ""; return url; } function link(text, url, image) { var u = safeUrl(url); if (!u) return image ? esc("![" + text + "]") : inline(text); if (image) return "" + escAttr(text) + ""; return "" + inline(text) + ""; } /* Inline rendering over one block of text. Scans to the next marker character * and bulk-escapes the plain text in between, so it stays roughly linear. */ function inline(s) { var out = "", i = 0, n = s.length, marker = /[`!\[*_]/g, m, rest; while (i < n) { marker.lastIndex = i; m = marker.exec(s); if (!m) { out += esc(s.slice(i)); break; } if (m.index > i) { out += esc(s.slice(i, m.index)); i = m.index; } rest = s.slice(i); if ((m = /^`([^`]+)`/.exec(rest))) out += "" + esc(m[1]) + ""; else if ((m = /^!\[([^\]]*)\]\(\s*([^)\s]+)[^)]*\)/.exec(rest))) out += link(m[1], m[2], true); else if ((m = /^\[([^\]]*)\]\(\s*([^)\s]+)[^)]*\)/.exec(rest))) out += link(m[1], m[2], false); else if ((m = /^(\*\*|__)([\s\S]+?)\1/.exec(rest))) out += "" + inline(m[2]) + ""; else if ((m = /^(\*|_)([^\s][\s\S]*?)\1/.exec(rest))) out += "" + inline(m[2]) + ""; else { out += esc(s.charAt(i)); i++; continue; } i += m[0].length; } return out; } function cells(row) { return row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map(function (c) { return c.trim(); }); } function render(src) { var lines = src.replace(/\r\n?/g, "\n").split("\n"); var out = "", i = 0, n = lines.length, line, m, k; while (i < n) { line = lines[i]; if (/^\s*$/.test(line)) { i++; continue; } if ((m = /^\s*(`{3,}|~{3,})\s*([\w.+#-]*)/.exec(line))) { var fence = m[1].charAt(0) === "`" ? /^\s*`{3,}\s*$/ : /^\s*~{3,}\s*$/; var lang = m[2], code = ""; for (i++; i < n && !fence.test(lines[i]); i++) code += lines[i] + "\n"; i++; out += "
" + esc(code) + "
"; continue; } if ((m = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line))) { k = m[1].length; out += "" + inline(m[2]) + ""; i++; continue; } if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) { out += "
"; i++; continue; } if (/^\s*>/.test(line)) { var q = ""; for (; i < n && /^\s*>/.test(lines[i]); i++) q += lines[i].replace(/^\s*>\s?/, "") + "\n"; out += "
" + render(q) + "
"; continue; } if (line.indexOf("|") >= 0 && i + 1 < n && /^\s*\|?(\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?\s*$/.test(lines[i + 1])) { var head = cells(line), t = ""; for (k = 0; k < head.length; k++) t += ""; t += ""; for (i += 2; i < n && lines[i].indexOf("|") >= 0 && !/^\s*$/.test(lines[i]); i++) { var row = cells(lines[i]); t += ""; for (k = 0; k < row.length; k++) t += ""; t += ""; } out += t + "
" + inline(head[k]) + "
" + inline(row[k]) + "
"; continue; } if (/^\s*([-*+]|\d+[.)])\s+/.test(line)) { var ordered = /^\s*\d/.test(line), tag = ordered ? "ol" : "ul"; out += "<" + tag + ">"; for (; i < n && (m = /^\s*([-*+]|\d+[.)])\s+(.*)$/.exec(lines[i])); i++) { if ((/\d/.test(m[1])) !== ordered) break; out += "
  • " + inline(m[2]) + "
  • "; } out += ""; continue; } /* Always consume the current line so i advances even when it * matched none of the block branches above. */ var para = lines[i++]; for (; i < n && !/^\s*$/.test(lines[i]) && !/^\s*(#{1,6}\s|>|`{3,}|~{3,}|([-*+]|\d+[.)])\s)/.test(lines[i]); i++) para += "\n" + lines[i]; out += "

    " + inline(para).replace(/\n/g, "
    ") + "

    "; } return out; } document.addEventListener("DOMContentLoaded", function () { var nodes = document.querySelectorAll("div#cgit [data-markdown]"), i, el, text; for (i = 0; i < nodes.length; i++) { el = nodes[i]; text = el.textContent; if (!text || text.length > MAX_BYTES) continue; try { el.innerHTML = render(text); /* The attribute doubles as the style hook for the * unrendered source, so drop it once rendered. */ el.removeAttribute("data-markdown"); } catch (e) { /* leave the escaped source in place on any failure */ } } }, false); })();