commit 13798906ceedfc1473643d06f4f98d4f372e889e
parent 45093b8a6bf4116254bb4931844a03fb8334b0ba
Author: Sam Tobin-Hochstadt <samth@cs.indiana.edu>
Date: Thu, 30 Apr 2020 16:22:17 -0400
Automatically link to source code of document.
This works, provided that:
- the package is on the catalog at pkgs.racket-lang.org
- the package is hosted on GitHub or GitLab
Restriction 2 could be lifted for other known sites or packages
hosted as directories.
Restriction 1 would be harder to lift.
This only links to the _top level_ file that defines the overall
document, not the file defining the particular section. Fixing that
would require the Scribble renderer to provide more detailed
information, although it's certainly a possible extension.
Closes #223
Closes #208
Closes racket/racket#874
Related to #76, #228
Diffstat:
1 file changed, 170 insertions(+), 21 deletions(-)
diff --git a/scribble-lib/scribble/manual-racket.js b/scribble-lib/scribble/manual-racket.js
@@ -15,14 +15,170 @@ AddOnLoad(function() {
}
})
+// 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");
- var info = document.createElement("div");
info.className = "RPartExplain";
/* The "top" tag refers to a whole document: */
@@ -42,35 +198,28 @@ function AddPartTitleOnClick(elem) {
var line1x = ((is_long && prefixes) ? document.createElement("div") : line1);
var line2 = (is_long ? document.createElement("div") : line1);
- function add(dest, str, cn) {
- var s = document.createElement("span");
- s.className = cn;
- s.style.whiteSpace = "nowrap";
- s.appendChild(document.createTextNode(str));
- dest.appendChild(s);
- }
/* Construct a `secref` call with suitable syntax coloring: */
- add(line1, "\xA0@", "RktRdr");
- add(line1, (is_top ? "other-doc" : "secref"), "RktSym");
- add(line1, "[", "RktPn");
+ addSpan(line1, "\xA0@", "RktRdr");
+ addSpan(line1, (is_top ? "other-doc" : "secref"), "RktSym");
+ addSpan(line1, "[", "RktPn");
if (!is_top)
- add(line1, tag, "RktVal");
+ addSpan(line1, tag, "RktVal");
if (is_long) {
/* indent additional lines: */
if (prefixes)
- add(line1x, "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0", "RktPn");
- add(line2, "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0", "RktPn");
+ addSpan(line1x, "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0", "RktPn");
+ addSpan(line2, "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0", "RktPn");
}
if (prefixes) {
- add(line1x, " #:tag-prefixes ", "RktPn");
- add(line1x, "'", "RktVal");
- add(line1x, prefixes, "RktVal");
+ addSpan(line1x, " #:tag-prefixes ", "RktPn");
+ addSpan(line1x, "'", "RktVal");
+ addSpan(line1x, prefixes, "RktVal");
}
if (!is_top)
- add(line2, " #:doc ", "RktPn");
- add(line2, "'", "RktVal");
- add(line2, mod_path, "RktVal");
- add(line2, "]", "RktPn");
+ addSpan(line2, " #:doc ", "RktPn");
+ addSpan(line2, "'", "RktVal");
+ addSpan(line2, mod_path, "RktVal");
+ addSpan(line2, "]", "RktPn");
info.appendChild(line1);
if (is_long)