UNB/ CS/ David Bremner/ teaching/ cs4613/ docs/ manual-racket.js
/* For the Racket manual style */

AddOnLoad(function() {
    /* Look for header elements that have x-source-module and x-part tag.
       For those elements, add a hidden element that explains how to
       link to the section, and set the element's onclick() to display
       the explanation. */
    var tag_names = ["h1", "h2", "h3", "h4", "h5"];
    for (var j = 0; j < tag_names.length; j++) {
        elems = document.getElementsByTagName(tag_names[j]);
        for (var i = 0; i < elems.length; i++) {
            var elem = elems.item(i);
            AddPartTitleOnClick(elem);
        }
    }
})

// cache of source urls
var cache = {};

function ParseSource(source, mod_path, single_collection) {

    var source_url = new URL(source);

    if (source_url.protocol == "github:") {
        // browser URL parser only works with http(s) URLs
        source_url = new URL("https" + source.substring(6));
        var host = source_url.host;
        var url_path = source_url.pathname.substring(1).split("/");
        if (!(url_path.length >= 2)) return null;
        var user = url_path.shift();
        var repo = url_path.shift();
        var branch = url_path.shift();
        var source_path = url_path.join("/");
    }
    else if (("https:" == source_url.protocol) || ("git:" == source_url.protocol)) {
        // browser URL parser only works with http(s) URLs
        if ("git:" == source_url.protocol)
            source_url = new URL("https" + source.substring(3));

        var host = source_url.host;
        var source_path = source_url.searchParams.get("path");
        var branch = (source_url.hash || "#master").substring(1);
        var url_path = source_url.pathname.substring(1).split("/");
        if (url_path.length < 2) throw [source_url.pathname, url_path];
        var user = url_path.shift();
        var repo = url_path.shift();
        var mtch = repo.match(/(.*)\.git$/);
        if (mtch) repo = mtch[1];

    }
    else return null;

    var mod_path_re = /^\(lib "(.+)"\)$/;

    var mod_path_elems = mod_path && mod_path.match(mod_path_re)[1].split("/");

    if (!user || !repo || !mod_path_elems)
        return null;
    if (single_collection)
        mod_path_elems.shift();

    var file_path = mod_path_elems.join("/");


    if (source_path) {
        file_path = source_path + "/" + file_path;
    }

    return { user: user,
             repo: repo,
             file_path: file_path,
             branch: branch,
             host: host };
}

function AddSourceElement(pkg_url, info) {
    info.appendChild(document.createTextNode("Document source "));
    var url_line = document.createElement("div");
    var a = document.createElement("a");
    a.href = pkg_url;
    a.style.whiteSpace = "nowrap";
    a.appendChild(document.createTextNode(pkg_url));
    addSpan(url_line, "\xA0", "RktRdr");
    url_line.appendChild(a);
    info.appendChild(url_line);
}

var prefixes = { "github.com": "tree",
                 "gitlab.com": "-/blob" };


function AddSourceUrl(source, mod_path, collection, info) {
    // multi is encoded as an array, empty as false
    single_collection = (typeof collection === "string");

    var parsed = source && mod_path && ParseSource(source, mod_path, single_collection);

    if (!parsed) return;

    prefix = prefixes.hasOwnProperty(parsed.host) && prefixes[parsed.host];
    if (!prefix) return;

    var correct_url = "https://" + [parsed.host, parsed.user, parsed.repo, prefix, parsed.branch, parsed.file_path].join("/");

    if (info) AddSourceElement(correct_url, info);
}

function addSpan(dest, str, cn) {
    var s = document.createElement("span");
    s.className = cn;
    s.style.whiteSpace = "nowrap";
    s.appendChild(document.createTextNode(str));
    dest.appendChild(s);
}


// test cases
if (false) {
    console.log(ParseSource("git://gitlab.com/benn/foo?path=xxx",
                            '(lib "asn1/scribblings/asn1.scrbl")',
                            false))
    console.log(ParseSource("github://github.com/carl-eastlund/mischief/master",
                            '(lib "asn1/scribblings/asn1.scrbl")',
                            false))
    console.log(ParseSource("github://github.com/carl-eastlund/mischief/stable/dir",
                            '(lib "asn1/scribblings/asn1.scrbl")',
                            false))

    console.log(ParseSource("git://github.com/racket/racket/?path=pkgs/racket-doc",
                            '(lib "asn1/scribblings/asn1.scrbl")',
                            false));

    console.log(ParseSource("git://github.com/rmculpepper/asn1.git?path=asn1-doc",
                            '(lib "asn1/scribblings/asn1.scrbl")',
                            true));
    console.log(ParseSource("git://github.com/rmculpepper/asn1",
                            '(lib "asn1/scribblings/asn1.scrbl")',
                            true));
    console.log(ParseSource("git://github.com/rmculpepper/asn1",
                            '(lib "asn1/scribblings/asn1.scrbl")',
                            false));
}

function AddPartTitleOnClick(elem) {
    var mod_path = elem.getAttribute("x-source-module");
    var tag = elem.getAttribute("x-part-tag");
    var source_pkg = elem.getAttribute("x-source-pkg");

    // create here to share
    var info = document.createElement("div");


    // tag is not needed, but this way we can add the element in only one place
    // avoid failing on browser that don't have `fetch`
    if (mod_path && source_pkg && tag && window.fetch) {

        var cached = cache[mod_path]
        if (cached) {
            AddSourceElement(cached[0], mod_path, cached[1], info);
        }
        else {
            fetch("https://pkgs.racket-lang.org/pkg/" + source_pkg + ".json")
                .then(function (response) { return response.json(); })
                .then(function (data) {
                    var vers = data["versions"] || {};
                    var def = vers["default"] || {};
                    var source = def["source"] || undefined;
                    var collection = data["collection"];
                    if (source) {
                        cache[mod_path] = [source, collection];
                        AddSourceUrl(source, mod_path, collection, info);
                    }
                });
        }
    }

    if (mod_path && tag) {
        // Might not be present:
        var prefixes = elem.getAttribute("x-part-prefixes");

        info.className = "RPartExplain";

        /* The "top" tag refers to a whole document: */
        var is_top = (tag == "\"top\"");
        info.appendChild(document.createTextNode("Link to this "
                                                 + (is_top ? "document" : "section")
                                                 + " with "));

        /* Break `secref` into two lines if the module path and tag
           are long enough: */
        var is_long = (is_top ? false : ((mod_path.length
                                          + tag.length
                                          + (prefixes ? (16 + prefixes.length) : 0))
                                         > 60));

        var line1 = document.createElement("div");
        var line1x = ((is_long && prefixes) ? document.createElement("div") : line1);
        var line2 = (is_long ? document.createElement("div") : line1);

        /* Construct a `secref` call with suitable syntax coloring: */
        addSpan(line1, "\xA0@", "RktRdr");
        addSpan(line1, (is_top ? "other-doc" : "secref"), "RktSym");
        addSpan(line1, "[", "RktPn");
        if (!is_top)
            addSpan(line1, tag, "RktVal");
        if (is_long) {
            /* indent additional lines: */
            if (prefixes)
                addSpan(line1x, "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0", "RktPn");
            addSpan(line2, "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0", "RktPn");
        }
        if (prefixes) {
            addSpan(line1x, " #:tag-prefixes ", "RktPn");
            addSpan(line1x, "'", "RktVal");
            addSpan(line1x, prefixes, "RktVal");
        }
        if (!is_top)
            addSpan(line2, " #:doc ", "RktPn");
        addSpan(line2, "'", "RktVal");
        addSpan(line2, mod_path, "RktVal");
        addSpan(line2, "]", "RktPn");

        info.appendChild(line1);
        if (is_long)
            info.appendChild(line1x);
        if (is_long)
            info.appendChild(line2);

        info.style.display = "none";

        /* Add the new element afterthe header: */
        var n = elem.nextSibling;
        if (n)
            elem.parentNode.insertBefore(info, n);
        else
            elem.parentNode.appendChild(info);

        /* Clicking the information button shows the explanation element: */
        const heading = elem.querySelector('.heading-source');
        if (heading) {
          heading.onclick = function () {
            if (info.style.display === "none")
                info.style.display = "block";
            else
                info.style.display = "none";
          }
        }
    }
}