// Documentation licensed under CC BY 4.0 // License available at https://creativecommons.org/licenses/by/4.0/ // TODO(user): Bring in Closure library and compiler. var closure = window.closure || {}; closure.docs = closure.docs || {}; /** @define {string} */ closure.docs.LOCATION = String(window.location); /** * Returns a value from the global `_JEKYLL_DATA` object, which contains the * 'page' and 'site' data from Jekyll. This allows to configure the JS * behavior from the frontmatter. It is effectively `goog.getObjectByName`, * except we're not currently depending on Closure. * @param {string} param * @return {*} Result, or undefined. */ closure.docs.get = function(param) { var data = window['_JEKYLL_DATA']; return data && data[param]; }; /** * Applies the given function to each element. * @param {string} selector A query selector. * @param {function(!Element)} func */ closure.docs.forEachElement = function(selector, func) { var elements = document.querySelectorAll(selector); for (var i = 0, length = elements.length; i < length; i++) { func(elements[i]); } }; /** * Runs a callback on each heading. * @param {function(!Element, number)} func */ closure.docs.forEachHeading = function(func) { closure.docs.forEachElement( 'article > h1, article > h2, article > h3, ' + 'article > h4, article > h5, article > h6', function(heading) { var match = /^h(\d)$/i.exec(heading.tagName); func(heading, Number(match[1])); }); }; /** * Adds a scroll listener to the document. * The listener adds a "scrolled" and a "down" class to the body element * to indicate (respectively) whether the page is scrolled at all, and * whether the last scroll was down. */ closure.docs.addScrollListener = function() { // Add a scroll listener to handle body.scrolled and body.down var last = void 0; // Height difference between the scrolled and unscrolled header bars var threshold = 140; document.addEventListener('scroll', function() { var top = document.body.scrollTop; document.body.classList.toggle('scrolled', top > threshold); document.body.classList.toggle('down', top > last); last = top; }); }; /** * Add a listener so that clicking on #-only links calls scrollToHash * instead of the browser default. */ closure.docs.interceptLinkClicks = function() { /** * Scrolls to the window's current hash. Note: this is required * because of the {position: fixed} banner at the top, which will * cover the heading if we let the browser scroll on its own. * Instead, we add a delta to ensure that the heading ends up * below the banner. */ function scrollToHash() { var hash = window.location.hash.substring(1); if (hash) { var el = document.getElementById(hash); var delta = document.body.classList.contains('scrolled') ? 72 : 128; document.body.scrollTop = el.offsetTop - delta; } } document.addEventListener('click', function(e) { if (!e.target || e.target.tagName != 'A') return; var href = e.target.getAttribute('href'); if (href && href[0] == '#') { window.location.hash = href; requestAnimationFrame(scrollToHash); e.preventDefault(); } }); // Also scroll to hash on initial page load. requestAnimationFrame(scrollToHash); }; /** * Removes the first

header in the article and writes it into * the header and title. This should be done before building the * TOC so that the title doesn't show up as an entry. */ closure.docs.findTitle = function() { // Note: we need to skip the first (#top_of_page) element. var h1 = document.querySelectorAll('article > h1')[1]; if (h1) { var pageTitle = h1.textContent; h1.remove(); var title = document.querySelector('title'); if (!title.textContent) title.textContent = pageTitle; var heading = document.querySelector('h1#top_of_page'); if (heading && !heading.textContent) heading.textContent = pageTitle; } }; /** * Iterates over heading elements to add/correct numbers. * Anything that looks like a number will be adjusted. * Specifically, one can simply write "### 1.1" for all * headings and this function will fill in the correct * number. Also assigns IDs if one isn't already given. */ closure.docs.autoNumber = function() { var min = Number(closure.docs.get('page.toc.min') || 2); var nums = []; var ids = {}; closure.docs.forEachHeading(function(heading, level) { if (level < min) return; // Don't do any numbering unless the heading starts with a digit, // though we do still need to pop numbers off before incrementing. while (nums.length > level - min + 1) { nums.pop(); } if (!/^\d/.test(heading.textContent)) return; while (nums.length < level - min + 1) { nums.push(0); } nums[nums.length - 1]++; // Auto-generate an ID if necessary. if (!heading.id) { var base = '_' + heading.textContent.toLowerCase() .replace(/[^a-z]+/g, '-') .replace(/^-|-$/g, ''); var suffix = ''; while (base + suffix in ids) { suffix++; } heading.id = base + suffix; ids[base + suffix] = true; } // Correct the number. heading.textContent = heading.textContent.replace(/^\d+(\.\d+)*/, nums.join('.')); }); }; /** * Replaces the text content of intra-document links to match the * linked section's heading. This is necessary when auto-numbering * is used in order to get the right number in the text. It is * triggered by links whose text is exactly two or more question marks. */ closure.docs.fixLinkText = function() { closure.docs.forEachElement('a', function(link) { var href = link.getAttribute('href'); if (!/^#/.test(href) || !/^\?\?+$/.test(link.textContent)) return; var heading = document.getElementById(href.substring(1)); if (heading) link.textContent = heading.textContent; // TODO(user): allow including/excluding the number? }); }; /** * Builds the table of contents. This should run after * autoNumber so that the correct text makes it in. */ closure.docs.buildToc = function() { // Read a few page-level parameters to customize. var min = Number(closure.docs.get('page.toc.min') || 2); var max = Number(closure.docs.get('page.toc.max') || 3); // TODO(user): allow further customization of numbering? var stack = []; closure.docs.forEachHeading(function(heading, level) { if (level < min || level > max) return; var depth = level - min + 1; while (stack.length > depth) { stack.pop(); } while (stack.length < depth) { var list = document.createElement('ul'); // Add to the most recent 'li' item (unless this is the first entry). var prev = stack[stack.length - 1]; if (prev) { if (!prev.lastChild) prev.appendChild(document.createElement('li')); prev.lastChild.appendChild(list); } stack.push(list); } var item = document.createElement('li'); stack[stack.length - 1].appendChild(item); var link = document.createElement('a'); item.appendChild(link); link.href = '#' + heading.id; link.textContent = heading.textContent; }); // Finally add the toc to our toc elements. var toc = stack[0]; closure.docs.forEachElement('nav.toc ul', function(ul) { if (toc && toc.innerHTML) { ul.innerHTML += toc.innerHTML; } else { ul.parentElement.remove(); // don't bother with TOC if it's empty } }); }; /** * Fix some syntax highlighting. Rouge does a poor job highlighting JS. * It marks every identifier as 'nx' regardless of how it is used, whereas * GitHub-flavored markdown highlights the final identifier in a qualified * function name as a function. This function finds any 'nx' identifier * that is followed by an open-paren and changes it to 'nf'. */ closure.docs.fixSyntaxHighlighting = function() { closure.docs.forEachElement('.highlight .nx+.p', function(p) { if (p.textContent[0] == '(') p.previousElementSibling.className = 'nf'; }); }; /** * Highlights callouts. A callout is a paragraph that begins with * 'NOTE:' or 'TIP:' or 'WARNING:' (or several others). These are * highlighted by adding the 'callout-*' to the classlist, where * '*' is 'note', 'tip', 'warning', etc. */ closure.docs.highlightCallouts = function() { closure.docs.forEachElement('p', function(p) { var match = /^([A-Za-z]+):/.exec(p.textContent); if (match) p.classList.add('callout-' + match[1].toLowerCase()); }); }; /** * Sets the URL on the edit link. */ closure.docs.setEditLink = function() { var link = document.querySelector('a.edit'); var match = /\/\/([^.]+).github.io\/([^/]+)\/(.*)$/.exec(closure.docs.LOCATION); if (!match || !link) return; link.href = [ 'https://github.com', match[1], match[2], 'edit/master/doc', match[3] + '.md' ].join('/'); }; /** * Marks the current page and section as 'active' in nav menus. */ closure.docs.markActiveNav = function() { // Absolutize link var abs = (function() { var link = document.createElement('a'); return function(rel) { link.href = rel; return link.href; }; })(); // Checks for a prefix, returns everything after it if it exists var suffix = function(prefix, string) { return string.substring(0, prefix.length) == prefix ? string.substring(prefix.length) : ''; }; // Figure out the current page/section. var location = closure.docs.LOCATION; var page = location.replace(/\.(?:md|html)?/, ''); var section = location.substring(0, location.lastIndexOf('/')); // If section was overridden in the page frontmatter, use that instead. var sectionParameter = closure.docs.get('page.section'); if (sectionParameter != null) { var root = closure.docs.get('site.baseurl;') || '/'; if (root.length > 1 && root[root.length - 1] == '/') { root = root.substring(0, root.length - 1); } section = abs(root + '/' + sectionParameter.replace(/^\/|\/$/g, '')); } // Set links active if we're currently visiting them. closure.docs.forEachElement('header nav a', function(a) { if (/^\/[^/]*$/.test(suffix(section, a.href))) { a.classList.add('active'); } }); closure.docs.forEachElement('nav.side a', function(a) { if (/^(\.html|\.md)?$/.test(suffix(page, a.href))) { a.classList.add('active'); } }); }; /** * Kicks off Google Analytics. This is just a pretty-printed * version of the standard installation code. * @suppress {checkTypes} */ closure.docs.startAnalytics = function() { var productKey = closure.docs.get('page.ga'); if (!productKey) return; (function(i, s, o, g, r, a, m) { i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function() { (i[r].q = i[r].q || []).push(arguments); }, i[r].l = 1 * new Date(); a = s.createElement(o), m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m); })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); window['ga']('create', productKey, 'auto'); window['ga']('send', 'pageview'); }; /** * Initialize everything. */ closure.docs.initialize = function() { closure.docs.findTitle(); closure.docs.autoNumber(); closure.docs.buildToc(); closure.docs.fixLinkText(); closure.docs.fixSyntaxHighlighting(); closure.docs.highlightCallouts(); closure.docs.markActiveNav(); closure.docs.addScrollListener(); closure.docs.interceptLinkClicks(); closure.docs.setEditLink(); closure.docs.startAnalytics(); };