// Perforce Documentation Javascript // // vim: set ts=2 sw=2 tw=80 ai si: $("#header button.search").css("display", "block"); var wNSC = '-=-'; !function ($) { $(function() { var $window = $(window), $body = $(document.body), navHeight = $("#header").outerHeight(true), preName; // generate the sidebar toc p4.generateSidebar(); // handle nav pane scroll position var navInfo = false; var navScroll = 0; var navItemPos = 0; // if a nav scroll position has been recorded, use it if (window.name.search('^'+ window.location.host +wNSC+ '(\\d+)' +wNSC+ '(\\d+)') == 0) { var zname = window.name.split(wNSC); navItemPos = Number(zname[1]); navScroll = Number(zname[2]); navInfo = true; window.name = zname.slice(3).join(wNSC); $("#nav > ul").scrollTop(navScroll); } // determine if we have an active item in the nav pane var actives = $("#nav.toc .active"); if (actives.length) { var last = actives[actives.length - 1]; var itemPos = $(last).offset().top; if (itemPos !== undefined) { if (navInfo) { navScroll = navScroll + itemPos - navItemPos; } else { var parentPos = $("#nav > ul").offset().top; navScroll = itemPos - parentPos; } if (navScroll < 0) navScroll = 0; } } if (navScroll > 0) { $("#nav > ul").scrollTop(navScroll); } $("#nav .cover").hide(); // hookup ScrollSpy $("body").scrollspy({ target: '#nav', offset: navHeight + 40, }).focus(); // hookup swiping for navigation $window.touchwipe({ wipeLeft: function() { if (!$("link[rel='next']").length) return; e.preventDefault(); window.location = $("link[rel='next']").attr("href"); }, wipeRight: function() { if (!$("link[rel='prev']").length) return; e.preventDefault(); window.location = $("link[rel='prev']").attr("href"); }, preventDefaultEvents: false }); // hookup reading progress $(document).on("scroll", function (e) { var contentHeight = $("#content").innerHeight(), headerHeight = $("#header").innerHeight(), w = $(window), wHeight = w.innerHeight(), scrolled = Math.max(w.scrollTop() - headerHeight, 0), area = Math.max(contentHeight - wHeight, 1), percentage = Math.round(100 * scrolled / area) + "%"; $("#progress").css({ width: percentage }); }); // estimate reading time var totalWords = $("#content").text().trim().split(/\s+/g).length; var timeEst = Math.ceil(totalWords / 240); $("#content .container").prepend( $('<div class="eta"><span class="time">' + p4.t("Reading time: about ") + timeEst + ' ' + (timeEst != 1 ? p4.t("minutes") : p4.t("minute")) + '</span> <span class="words">(' + p4.formatThousands(totalWords) + ' ' + p4.t("words") + ')</span></div>') ); // hookup menu $("button.toc").on("click", function (e) { e.preventDefault(); p4.toggleMenu(); }); $("a.expander").on("click", function (e) { e.preventDefault(); var pNode = $(this).parent("li"), ulNode = $(this).next("ul"); if (pNode.hasClass("expanded")) { pNode.removeClass("expanded"); ulNode.css("maxHeight", ""); } else { pNode.addClass("expanded"); var ulHeight = 0; ulNode.find("li").each(function () { ulHeight += $(this).height(); }); ulNode.css("maxHeight", ulHeight + "px"); } }); // hookup search $("button.search").on("click", function (e) { e.preventDefault(); p4.toggleSearch(); }); $("button.clear").on("click", function (e) { e.preventDefault(); $("#search-text").val(""); p4.searcher.clear(); }); $("#search-text").on("keyup", function (e) { var results = p4.searcher.search($("#search-text").val()); p4.searcher.unhighlight(); p4.searcher.render(results); p4.searcher.highlight(p4.searcher.theWords); }); $("#search input.substring").on("change", function (e) { var results = p4.searcher.search($("#search-text").val()); p4.searcher.render(results); }); $("#search input.highlight").on("change", function (e) { p4.searcher.toggleHighlight(); }); p4.searcher.highlight(); // record nav scroll on nav link click $("#nav ul.toc a[href]").on("click", function (e) { if (typeof(preName) === 'undefined') preName = window.name; var curLoc = location.pathname.split('/').reverse()[0]; var hrefLoc = $(this).attr("href").split('#')[0]; if (curLoc !== hrefLoc) { var navScroll = $("#nav > ul").scrollTop(); var itemPos = $(this).parent().offset().top; var newName = window.location.host +wNSC+ itemPos +wNSC+ navScroll +wNSC+ preName; window.name = newName; } }); // setup keyboard help dialog var helpDialogHTML = [ '<div>', ' <a href="#myModal" id="hotkeys" role="button" data-toggle="modal"></a>', ' <div id="myModal" class="modal fade in" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="false">', ' <div class="modal-dialog">', ' <div class="modal-content">', ' <div class="modal-header">', ' <button type="button" class="close" data-dismiss="modal">×</button>', ' <h4 id="myModalLabel" class="modal-title">' + p4.t("Documentation Keyboard Shortcuts") + '</h4>', ' </div>', ' <div class="modal-body">', ' <dl>', ' <dt>?</dt>', ' <dd>' + p4.t("Show this help") + ' (<b>' + p4.t("ESC") + '</b> ' + p4.t("to close") + ')</dd>', ' <dt>n ' + p4.t("or") + ' <i class="glyphicon glyphicon-arrow-right"/></dt>', ' <dd>' + p4.t("Next page") + '</dd>', ' <dt>p ' + p4.t("or") + ' <i class="glyphicon glyphicon-arrow-left"/></dt>', ' <dd>' + p4.t("Previous page") + '</dd>', ' <dt>t</dt>', ' <dd>' + p4.t("Scroll to top of current page") + '</dd>', ' <dt>h</dt>', ' <dd>' + p4.t("Go to the home page of this guide") + '</dd>', ' <dt>u</dt>', ' <dd>' + p4.t("Go up one level in this guide") + '</dd>', ' <dt>s ' + p4.t("or") + ' f</dt>', ' <dd>' + p4.t("Open the search sidebar") + '</dd>', ' <dt>c</dt>', ' <dd>' + p4.t("Clear the search term(s)") + '</dd>', ' </dl>', ' </div>', ' </div>', ' </div>', ' </div>', '</div>' ].join("\n"); $("body").append(helpDialogHTML); // hookup keyboard doc navigation $(document).on("keydown", function(e) { // don't act on special keys, already handled events, or text input areas if (e.altKey || e.ctrlKey || e.metaKey || e.isDefaultPrevented() || $(e.target).is("input, textarea, select") ) { return; } switch (e.which) { case 191: // ? if (e.shiftKey) { $("#hotkeys").trigger("click"); } break; case 78: // n case 39: // right-arrow if ($("link[rel='next']").length) { e.preventDefault(); window.location = $("link[rel='next']").attr("href"); } break; case 80: // p case 37: // left-arrow if ($("link[rel='prev']").length) { e.preventDefault(); window.location = $("link[rel='prev']").attr("href"); } break; case 85: // u if ($("link[rel='up']").length) { e.preventDefault(); window.location = $("link[rel='up']").attr("href"); } break; case 72: // h if ($("link[rel='top']").length) { e.preventDefault(); window.location = $("link[rel='top']").attr("href"); } break; case 84: // t e.preventDefault(); $("#content").scrollTop(0); break; case 70: // f case 83: // s e.preventDefault(); p4.toggleSearch(); break; case 67: // c e.preventDefault(); p4.searcher.clear(); break; } }); // hookup prettify $("pre.programlisting").each(function () { var $this = $(this), lang = $this.attr("lang"); if (lang) { $this.addClass("lang-" + lang); if (lang !== "bash") { $this.addClass("prettyprint"); } } }); window.prettyPrint(); // fix up images in lists of instructions $("span.thumb img").each(function () { $(this).attr("title", $(this).attr("alt")); }) $("span.popup img").each(function () { var title = $(this).attr("alt") ? ": " + $(this).attr("alt") : ""; $(this).attr("title", p4.t("Click to expand") + title); var expander = $('<span class="expand">' + p4.t("Click to expand") + ' <i class="glyphicon glyphicon-fullscreen"></i></span>'); $(this).parent().append(expander); }); $(document).on("click", "span.popup img,span.popup .expand", function (e) { p4.docShowPopup($("img", $(this).parent())); }); $(document).on("click", ".doc-image-popup", function(e) { p4.docHidePopup(); }); // make external reference links open a new browser tab/window $("a.link[href^=http]").each(function () { $(this).attr("target", "_blank"); }); // tweak the offset to links with hashtags $('a[href*=#]:not([href=#]):not([href^=javascript])').click(function (e) { if ( location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname ) { var zhash = this.hash.split('#')[1]; p4.animateScroll(p4.hashScrollPos(zhash)); } }); // Scroll on hash change $(window).on('hashchange', p4.scrollToHash); // Scroll on page load with URL containing an anchor tag. p4.scrollToHash(); }); }(window.jQuery); // ensure we have an Object.keys() method if (!Object.keys) Object.keys = function(o) { if (o !== Object(o)) throw new TypeError("Object.keys called on non-object"); var ret=[],p; for (p in o) { if (Object.prototype.hasOwnProperty.call(o, p)) ret.push(p); } return ret; } // provide an array intersection function $.arrayIntersect = function(a, b) { return $.grep(a, function(i) { return $.inArray(i, b) > -1; }); } // add a case-insensitive Contains method jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function(arg) { return function( elem ) { return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; }; }); // ==================================================================== // Customized Bootstrap ScrollSpy for Perforce documentation navigation // ==================================================================== +function ($) { "use strict"; // SCROLLSPY CLASS DEFINITION // ========================== function ScrollSpy(element, options) { var href var process = $.proxy(this.process, this) this.$element = $(element).is('body') ? $(window) : $(element) this.$body = $('body') this.$scrollElement = this.$element.on('scroll.bs.scroll-spy.data-api', process) this.options = $.extend({}, ScrollSpy.DEFAULTS, options) this.selector = (this.options.target || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 || '') + ' .nav li > a' this.offsets = $([]) this.targets = $([]) this.activeTarget = null this.refresh() this.process() } ScrollSpy.DEFAULTS = { offset: 10 } ScrollSpy.prototype.refresh = function () { var offsetMethod = this.$element[0] == window ? 'offset' : 'position' this.offsets = $([]) this.targets = $([]) var dURL = window.location.pathname.match(/([^\/]+)$/) var self = this var $targets = this.$body .find(this.selector) .map(function () { var $el = $(this) var href = $el.data('target') || $el.attr('href') var $href = /^#\w/.test(href) && $(href) var href2 = /\/?([^/#]+)(#.+)$/.exec(href); var $href2; if (href2 && href2[2] && dURL && href2[1] === dURL[1]) { $href2 = $(href2[2].replace(/\./g, "\\.")); } if (!$href2 || !$href2.length) { return null; } return ( [[ $href2[offsetMethod]().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href2[2] ]] ) || null; }) .sort(function (a, b) { return a[0] - b[0] }) .each(function () { self.offsets.push(this[0]) self.targets.push(this[1]) }) } ScrollSpy.prototype.process = function () { var scrollTop = this.$scrollElement.scrollTop() + this.options.offset var scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight var maxScroll = scrollHeight - this.$scrollElement.height() var offsets = this.offsets var targets = this.targets var activeTarget = this.activeTarget var i if (scrollTop >= maxScroll) { return activeTarget != (i = targets.last()[0]) && this.activate(i) } for (i = offsets.length; i--;) { activeTarget != targets[i] && scrollTop >= offsets[i] && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) && this.activate( targets[i] ) } } ScrollSpy.prototype.activate = function (target) { this.activeTarget = target $(this.selector) .parents('.active') .removeClass('active') var selector = this.selector + '[data-target$="' + target + '"],' + this.selector + '[href$="' + target + '"]' var active = $(selector) .parents('li') .addClass('active') if (active.parent('.dropdown-menu').length) { active = active .closest('li.dropdown') .addClass('active') } active.trigger('activate') } // SCROLLSPY PLUGIN DEFINITION // =========================== var old = $.fn.scrollspy $.fn.scrollspy = function (option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.scrollspy') var options = typeof option == 'object' && option if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) if (typeof option == 'string') data[option]() }) } $.fn.scrollspy.Constructor = ScrollSpy // SCROLLSPY NO CONFLICT // ===================== $.fn.scrollspy.noConflict = function () { $.fn.scrollspy = old return this } // SCROLLSPY DATA-API // ================== $(window).on('load', function () { $('[data-spy="scroll"]').each(function () { var $spy = $(this) $spy.scrollspy($spy.data()) }) }) }(window.jQuery); // ==================================================================== // Customized Search facility, adapted from DocBook WebHelp code // ==================================================================== p4 = {}; // localization setup p4.locale_text = { "Reading time: about": "Reading time: about", "minute": "minute", "minutes": "minutes", "words": "words", "Documentation Keyboard Shortcuts": "Documentation Keyboard Shortcuts", "Show this help": "Show this help", "ESC": "ESC", "to close": "to close", "or": "or", "Next page": "Next page", "Previous page": "Previous page", "Scroll to top of current page": "Scroll to top of current page", "Go to the home page of this guide": "Go to the home page of this guide", "Go up one level in this guide": "Go up one level in this guide", "Open the search sidebar": "Open the search sidebar", "Clear the search term(s)": "Clear the search term(s)", "Click to expand": "Click to expand", "Click to shrink": "Click to shrink", "Exact Matches": "Exact Matches", "Fuzzy Matches": "Fuzzy Matches", "Phrase Matches": "Phrase Matches", } p4.generateSidebar = function () { var i, ul = $('<ul class="toc nav"/>'); var curLoc = location.pathname.split('/').reverse()[0]; $.each(sidebar_toc.n, function (i) { var li = p4.makeNav(sidebar_toc.n[i], curLoc); li.appendTo(ul); }); ul.appendTo("#nav"); } p4.t = function(key) { if (p4.locale_text.hasOwnProperty(key)) { return p4.locale_text[key]; } return key; } p4.makeNav = function (nav, curLoc) { var active = (curLoc === nav.u) ? '<li class="active">' : "<li>"; var li = $(active + '<a href="' + nav.u +'"><span class="prefix"></span>'+ nav.t +'</a></li>'); if (nav.hasOwnProperty('n')) { var a = $('<a class="expander"><span class="glyphicon glyphicon-chevron-down"></span></a>'); a.appendTo(li); var i, ul = $('<ul class="nav"/>'); $.each(nav.n, function (i) { var subnav = nav.n[i]; var sli = p4.makeNav(subnav, curLoc); sli.appendTo(ul); }); ul.appendTo(li); } return li; } p4.revStr = function (str) { return (str + "").split('').reverse().join(''); } p4.formatThousands = function (num) { return p4.revStr(p4.revStr(num).replace(/(\d{3})(?=\d)(?!d*\.)/g,"$1,")); } p4.scrollToHash = function () { if (window.location.hash && window.location.hash.length) { p4.animateScroll(p4.hashScrollPos(window.location.hash.split('#')[1])); } } p4.animateScroll = function (scrollPos) { if (scrollPos < 0) scrollPos = 0; $("html,body").animate({ scrollTop: Math.floor(scrollPos) }, 300); } p4.hashScrollPos = function (zhash) { if (zhash === undefined || zhash.length == 0) return 0; var target; var vOffset = 45; // to position items below the header switch (zhash[0]) { case 'S': // scroll to a percentage of the content var pct = Math.floor(zhash.split("S")[1]); var container = $("#content .container"); return Math.floor( (container.height() + container.offset().top) * pct / 100 ); break; case 'P': // scroll to a specific paragraph var par = Math.floor(zhash.split("P")[1]); if (par > 0) { target = $("p, dt, dd, pre", "#content .container"); if (target !== undefined && target.length && target[par - 1] !== undefined ) { return $(target[par - 1]).offset().top - vOffset; } } break; case '/': // scroll to the first instance of a term var term = decodeURIComponent(zhash.split("/")[1]); if (term !== undefined && term.length) { target = $("#content .container *:Contains('"+ term +"')") .filter(function () { return $(this).children().length === 0; }); if (target !== undefined && target.length) { return $(target[0]).offset().top - vOffset; } } break; default: if (document.getElementById(zhash) !== null) { target = $('#' + zhash.replace(/\./g, "\\.")); if (target !== undefined && target.length) { return target.offset().top - 45; } } } return 0; }; p4.toggleMenu = function() { var elem = $("#nav"); if (elem.hasClass("open")) { elem.removeClass("open"); $("button.toc").removeClass("active"); } else { elem.addClass("open"); $("button.toc").addClass("active"); $("#search").removeClass("open"); $("button.search").removeClass("active"); } }; p4.toggleSearch = function() { var elem = $("#search"); if (elem.hasClass("open")) { elem.removeClass("open"); $("button.search").removeClass("active"); $("#search-text").blur(); } else { elem.addClass("open"); $("button.search").addClass("active"); $("#search-text").focus(); $("#nav").removeClass("open"); $("button.toc").removeClass("active"); } }; p4.docShowPopup = function (img) { p4.docHidePopup(); var title = $(img).attr("alt") ? ": " + $(img).attr("alt") : ""; var newImg = $(img).clone().attr("style", null).attr("title", p4.t("Click to shrink") + title); var popup = $('<div class="doc-image-popup"><span class="close-icon"></span></div>').prepend(newImg); if ($(img).closest("li")[0]) { $(img).closest("li").append(popup); } else { $(img).after(popup); } } p4.docHidePopup = function () { $('.doc-image-popup').remove(); } p4.searcher = { cookieName: "p4doc_search", reAlpha: /[^a-zA-Z]/g, reNumeric: /[^0-9]/g, theWords: "", tokens: (typeof document_index !== 'undefined') ? Object.keys(document_index["i"]) : {}, search: function(searchFor) { if (searchFor === undefined || searchFor.length < 2) return; var terms = this.wordList(searchFor), fuzzy_match = {}, exact_match = {}, starts_match = {}, ends_match = {}, x, y, z, i, j; if (terms.length < 1) return; // find all term matches for (x = 0; x < terms.length; x++) { var term = terms[x]; // exact match exact_match[term] = (document_index["i"][term] !== undefined) ? true : false; // fuzzy match fuzzy_match[term] = $.grep(this.tokens, function (item, index) { return (item.indexOf(term) > -1); }); // first term in a phrase is allowed to be and ends-with ends_match[term] = $.grep(this.tokens, function (item, index) { return (item.indexOf(term, item.length - term.length) > -1); }); // the last term in a phrase is allowed to be a starts-with starts_match[term] = $.grep(this.tokens, function (item, index) { return (item.indexOf(term) === 0); }); } // determine the set of documents containing all fuzzy matches var fuzzies = Object.keys(fuzzy_match), matching_docs = []; for (x = 0; x < document_index["f"].length; x++) { var matches = 0; for (y = 0; y < fuzzies.length; y++) { var term = fuzzies[y]; for (z = 0; z < fuzzy_match[term].length; z++) { var fuzzy_term = fuzzy_match[term][z]; if (document_index["i"][fuzzy_term][x] !== undefined) { matches += 1; break; } } } if (matches === fuzzies.length) { matching_docs.push(x); } } // determine which documents might have phrase matches var phrase_match = {}; if (terms.length > 1 && ends_match[terms[0]] !== undefined) { // find which documents might contain the phrase for (x = 0; x < matching_docs.length; x++) { var doc_id = matching_docs[x]; // prime the match count phrase_match[doc_id] = 0; // prime the list of starting word offsets from the list of tokens // that end with the first term var start_offsets = []; for (y = 0; y < ends_match[terms[0]].length; y++) { var end_term = ends_match[terms[0]][y], doc_offsets = document_index["i"][end_term][doc_id]; if (doc_offsets === undefined) continue; start_offsets = start_offsets.concat(doc_offsets); } // evaluate all of the starting offsets for (y = 0; y < start_offsets.length; y++) { var start_offset = start_offsets[y], found = 1; // discover whether all the remaining terms appear in sequence for (z = 1; z < terms.length; z++) { var term = terms[z], term_docs = document_index["i"][term]; // if this term is the last one, but there is no exact match // evaluate the starts with contenders if (z === terms.length - 1 && term_docs === undefined) { var starters = Object.keys(starts_match[term]), found_starter = 0; for (i = 0; i < starters.length; i++) { var starter = starts_match[term][i]; term_docs = document_index["i"][starter]; if (term_docs !== undefined && term_docs[doc_id] !== undefined) { var term_offsets = term_docs[doc_id]; for (j = 0; j < term_offsets.length; j++) { var term_offset = term_offsets[j]; if (term_offset === start_offset + z) { found_starter += 1; break; } } } } if (found_starter > 0) { found += 1; } break; } if (term_docs !== undefined && term_docs[doc_id] !== undefined) { var term_offsets = term_docs[doc_id]; for (i = 0; i < term_offsets.length; i++) { var term_offset = term_offsets[i]; if (term_offset === start_offset + z) { found += 1; break; } } } } // if all terms appear in sequence, this document contains the phrase if (found === terms.length) { phrase_match[doc_id] += 1; } } } } // compute scores var results = [], exact_results = [], fuzzy_results = [], phrase_results = [], pushed = [], total_docs = document_index["f"].length, exacts = Object.keys(exact_match); var stop_words = [ "a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "arent", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "cant", "cannot", "could", "couldnt", "did", "didnt", "do", "does", "doesnt", "doing", "dont", "down", "during", "each", "few", "for", "from", "further", "had", "hadnt", "has", "hasnt", "have", "havent", "having", "he", "hed", "hell", "hes", "her", "here", "heres", "hers", "herself", "him", "himself", "his", "how", "hows", "i", "id", "ill", "im", "ive", "if", "in", "into", "is", "isnt", "it", "its", "its", "itself", "lets", "me", "more", "most", "mustnt", "my", "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same", "shant", "she", "shed", "shell", "shes", "should", "shouldnt", "so", "some", "such", "than", "that", "thats", "the", "their", "theirs", "them", "themselves", "then", "there", "theres", "these", "they", "theyd", "theyll", "theyre", "theyve", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasnt", "we", "wed", "well", "were", "weve", "were", "werent", "what", "whats", "when", "whens", "where", "wheres", "which", "while", "who", "whos", "whom", "why", "whys", "with", "wont", "would", "wouldnt", "you", "youd", "youll", "youre", "youve", "your", "yours", "yourself", "yourselves", ]; var matching_count = matching_docs.length; for (x = 0; x < matching_count; x++) { var doc_id = matching_docs[x], exact_score = 0, fuzzy_score = 0, phrase_score = 0; // process exact term matches for (y = 0; y < exacts.length; y++) { var term = exacts[y], info = document_index["i"][term]; if (exact_match[term] && info[doc_id] !== undefined) { // var tf = Math.log(1 + info[doc_id].length), // df = Math.log( // 1 + // (total_docs / matching_count) // * (Object.keys(info).length / matching_count) // ); // exact_score += tf * df; // score based on frequency in this doc var nominal_score = 0; if (info[doc_id] !== undefined) { nominal_score = Object.keys(info[doc_id]).length; // frequency in this doc } // scale scoring based on doc frequency nominal_score = nominal_score * (total_docs / Object.keys(info).length); // scale scoring based on stop words if ($.inArray(term, stop_words)) { nominal_score = nominal_score * 0.01; } exact_score += nominal_score; } } // process fuzzy term matches for (y = 0; y < fuzzies.length; y++) { var term = fuzzies[y]; for (z = 0; z < fuzzy_match[term].length; z++) { var fuzzy_term = fuzzy_match[term][z], info = document_index["i"][fuzzy_term]; // skip if we have already scored the exact match for the term if (fuzzy_term === term) continue; if (info[doc_id] !== undefined) { // var tf = Math.log(1 + (fuzzy_term.length / term.length) * info[doc_id].length), // df = Math.log( // 1 + // (total_docs / matching_count) // * (Object.keys(info).length / matching_count) // ); // fuzzy_score += tf * df; var nominal_score = 0; if (info[doc_id] !== undefined) { nominal_score = Object.keys(info[doc_id]).length; // frequency in this doc } // scale scoring based on doc frequency nominal_score = nominal_score * (total_docs / Object.keys(info).length); // scale scoring based on stop words if ($.inArray(fuzzy_term, stop_words)) { nominal_score = nominal_score * 0.01; } // scale scoring based on fuzziness nominal_score = nominal_score * (term.length / fuzzy_term.length); fuzzy_score += nominal_score; } } } // process phrase match if (phrase_match[doc_id] !== undefined && phrase_match[doc_id] > 0) { // phrase_score = exact_score / phrase_match[doc_id]; phrase_score = exact_score * phrase_match[doc_id]; } var title = document_index["f"][doc_id]["title"]; title = title.replace(/ \/\/.*$/, ""), doc_score = exact_score + fuzzy_score; doc_score = exact_score + phrase_score; if (this.wantSubstring()) { doc_score += fuzzy_score; } if (phrase_score > 0) { phrase_results.push({ id: doc_id, url: document_index["f"][doc_id]['name'], path: document_index["f"][doc_id]['path'], title: title, score: doc_score, type: 'p', }); pushed.push(doc_id); } if (exact_score > 0) { if ($.inArray(doc_id, pushed) < 0) { exact_results.push({ id: doc_id, url: document_index["f"][doc_id]['name'], path: document_index["f"][doc_id]['path'], title: title, score: doc_score, type: 'e', }); pushed.push(doc_id); } } if (fuzzy_score > 0) { if ($.inArray(doc_id, pushed) < 0) { fuzzy_results.push({ id: doc_id, url: document_index["f"][doc_id]['name'], path: document_index["f"][doc_id]['path'], title: title, score: doc_score, type: 'f', }); pushed.push(doc_id); } } } // sort results, by score, finally by title exact_results.sort(this.resultSort); fuzzy_results.sort(this.resultSort); phrase_results.sort(this.resultSort); results = results.concat(phrase_results, exact_results); if (this.wantSubstring()) { results = results.concat(fuzzy_results); } return results; }, resultSort: function(a, b) { // compare scores // var diff = a.score - b.score; var diff = b.score - a.score; if (diff !== 0) return diff; // compare titles var aTitle = a.title.replace(this.reAlpha, ""), bTitle = b.title.replace(this.reAlpha, ""); if (aTitle === bTitle) { // compare titles numerically aTitle = parseInt(a.title.replace(this.reNumeric, ""), 10); bTitle = parseInt(b.title.replace(this.reNumeric, ""), 10); return aTitle === bTitle ? 0 : aTitle > bTitle ? 1 : -1; } else { return aTitle > bTitle ? 1 : -1; } }, // turn on highlighting of search terms highlight: function(searchText) { if (searchText === undefined) { searchText = $.cookie(this.cookieName); $("#search-text").val(searchText); var results = p4.searcher.search(searchText); p4.searcher.render(results); } if (searchText !== undefined) { var wordList = this.wordList(searchText); wordList = wordList.concat(wordList[0]); $("#content").highlight(wordList); } }, // render search results render: function(results) { if (results === undefined) results = []; var html = '', i, cur_type = '', types = { "e": p4.t("Exact Matches"), "f": p4.t("Fuzzy Matches"), "p": p4.t("Phrase Matches"), }; if (results.length > 0) { for (i = 0; i < results.length; i++) { var result = results[i]; if (result["type"] !== cur_type) { cur_type = result["type"]; html += '<li><span class="type">' + types[cur_type] + '</span></li>'; } html += $.templates( '<li>' + '<a href="{{:url}}">' + '<div class="title">{{:title}}</div>' + '</a>' + '</li>' ).render(result); } } $("#search .results").html('').append($(html)); $("#search .count .number").html(results.length); }, // toggle highlighting of search terms toggleHighlight: function () { if ($("#search input.highlight").is(":checked")) { this.highlight(p4.searcher.theWords); } else { this.unhighlight(); } }, // turn off highlighting of search terms unhighlight: function () { $("#content").unhighlight(); }, // remove duplicate, and shorter than 3 chars, array entries unique: function(list) { var hash = {}, uniq = [], i, l; for (i = 0, l = list.length; i < l; ++i) { if ((list[i] && list[i].length < 3) || hash.hasOwnProperty(list[i])) continue; uniq.push(list[i]); hash[list[i]] = 1; } return uniq; }, wantHighlight: function() { return ($("#search input.highlight").is(":checked")) ? true : false; }, wantSubstring: function() { var ret = ($("#search input.substring").is(":checked")) ? false : true; return ret; }, // convert a string into a space-separated list of words wordList: function(searchString) { // new variant var string = this.theWords; if (searchString !== undefined) { var words = $.trim(searchString).split(" "); words = $.map(words, function (word) { return $.trim( word.replace(/['"]/g, "") .replace(/\.$/, "") .replace(/ +/g, " ") ).toLowerCase(); }); string = this.theWords = words.join(" "); $.cookie(this.cookieName, string); } return string.split(" "); }, clear: function() { $.cookie(this.cookieName, ""); this.unhighlight(); this.render(); } };
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 13895 | Paul Allen | Copying using p4convert-docbook | ||
//guest/perforce_software/doc_build/main/perforce/assets/publicsite/js/perforce.js | |||||
#26 | 12837 | eedwards |
#review-1029519 Change the way that the TOC is generated for the HTML presentation: - no longer include the TOC in each generated HTML page. - generate a JSON file representing the TOC. - add javascript to read the TOC in JSON format, and generate the appropriate HTML. - Remove the noscript block that prevents using the documentation with javascript disabled; just hide the search button because you can't use search without javascript. |
||
#25 | 12832 | eedwards |
Revise the javascript used in the HTML presentation to make the embedded English strings easy to translate. Each translated guide would: - update the guide-specific XSLT to include something like: <xsl:template name="user.webhelp.foot.content"> <script type="text/javascript" src="js/guide.js"></script> </xsl:template> - edit the "guide.js" to reflect the actual name for the guide-specific javascript file to be included. - include in "guide.js": p4.locale_text = { "Reading time: about": "Reading time: about", "minute": "minute", "minutes": "minutes", "words": "words", "Documentation Keyboard Shortcuts": "Documentation Keyboard Shortcuts", "Show this help": "Show this help", "ESC": "ESC", "to close": "to close", "or": "or", "Next page": "Next page", "Previous page": "Previous page", "Scroll to top of current page": "Scroll to top of current page", "Go to the home page of this guide": "Go to the home page of this guide", "Go up one level in this guide": "Go up one level in this guide", "Open the search sidebar": "Open the search sidebar", "Clear the search term(s)": "Clear the search term(s)", "Click to expand": "Click to expand", "Click to shrink": "Click to shrink", "Exact Matches": "Exact Matches", "Fuzzy Matches": "Fuzzy Matches", "Phrase Matches": "Phrase Matches", } - Translate the right-hand side of the above messages. |
||
#24 | 12827 | eedwards |
Add an estimate of reading time to the top of each HTML page. Also add an indicator of reading progress to the fixed header. |
||
#23 | 12787 | eedwards |
#review-871968 Add additional hashtag processing to make it easier to craft URLs that target specific portions of the HTML documentation when the provided section markers are not specific enough: #Sxx - scroll to xx percent down the page. e.g. #S45 to scroll 45% down the page. %Px - scroll to the xth "paragraph" block on the page, where code blocks are considered paragraphs, e.g. #P7 to scroll to the 7th "paragraph" %/xx - scroll to near the first instance of the text "xx". Matching is case-insensitive, and the text should be URI encoded. Wildcards not supported. e.g. #/Known%20limitations @gnicol should review. Rendered HTML is available for experimentation: http://ee.perforce.ca/docs/swarm.14.3/admin.security.html#S45 http://ee.perforce.ca/docs/swarm.14.3/admin.security.html#P11 http://ee.perforce.ca/docs/swarm.14.3/admin.security.html#/known%20limitations |
||
#22 | 12785 | eedwards |
Follow-on to 895408: better handling of negative scrolling situation. |
||
#21 | 12784 | eedwards |
Improved nav pane scroll position when moving from page to page in a guide. The behaviour isn't perfect in all situations yet, but is notably better. |
||
#20 | 12781 | eedwards |
Correct the check for document_index so that if the index is not available, no javascript errors occur that inhibit the rest of the UI. |
||
#19 | 12778 | eedwards |
Remove the "aria-hidden" attribute from the documentation's "keyboard shortcuts" modal dialog, per @813797. For folks with screen readers, the close button should now be findable. |
||
#18 | 12777 | eedwards |
Follow-on to @871216: remove the invocation for HighlightJS, as this remains experimental and would need license approval. |
||
#17 | 12776 | eedwards |
#review-851775 Updates to the HTML presentation: - remove the double DOCTYPE declarations. - revise markup so that the content is no longer in an immovable container. This should allow the scroll position to be maintained by the browser and work as expected after users navigate within a page and then use the browser's back button. - rename the generated search index file to 'index.js' to avoid a mimetype issue. Also, made the search code tolerant of a missing index.js. - fix scrolling to targeted locations in a guide by applying an offset to the scroll to move the target out from under the header, and to animate the scroll. |
||
#16 | 12774 | eedwards |
#review-847001 Fix for incorrect offset to first matching highlighted search term. |
||
#15 | 12770 | eedwards |
Remove a stray 's' from the doc keyboard shortcuts modal dialog definition. This is normally not visible, but would appear if the HTML page was printed. |
||
#14 | 12766 | eedwards |
Updates to the HTML presentation for docs to better support users with Internet Explorer 8: - removed used of HTML5 tags, such as header, footer and nav, which IE8 doesn't understand at all. - add Respond.js (MIT license), which simulates CSS media queries in IE8. - fix some issues with the javascript that IE8 could not handle. - add a custom IE8 stylesheet to include some very specific tweaks. The HTML for the following two guides has been updated, and is available for review for a limited time: http://ee.perforce.ca/docs/p4dist.ie8/ http://ee.perforce.ca/docs/swarm/ |
||
#13 | 12761 | eedwards |
Adjust the prettyprint logic to only be invoked when a language has been specified, and that language is not bash. This avoids having to specify "bash" for non-bash content just to avoid inappropriate syntax highlighting. |
||
#12 | 12760 | eedwards |
The code that scrolls the first highlighted content into view was including the offset for the highlight toggle control in the search sidebar. This change makes the code search for highlights within the content exclusively. |
||
#11 | 12758 | eedwards |
Updates to the HTML doc presentation's search interface: - Add checkbox to toggle whether partial match results will be displayed or not - Replace obscure water droplet icon with checkbox and label to make it obvious that clicking toggles the highlighting of search terms in the current page - Report results by type: phrase matches, exact term matches, and partial matches, using a heading to indicate each group. |
||
#10 | 12752 | eedwards |
Several doc presentation/interactiontweaks: - on page load, the navigation sidebar is scrolled to show the current section (as well as can be expected) - refactored the keypress handling to use a switch statement. This makes it easier to handle multiple keys with the same logic. - add a modal dialog describing the keyboard shortcuts available while viewing the docs. |
||
#9 | 12749 | eedwards |
Adjustments to make the documentation presentation very similar to the public web site. Notable changes: - fix scrollSpy logic that was failing to match section identifiers containing periods - the navigation footer stays in view at all sizes - the background, colours, font sizes, etc. are now identical/similar to the public web site. - we cannot currently use the public web site's fonts due to a licensing issue. |
||
#8 | 12746 | eedwards |
Follow-on to @778544: disable syntax highlighting for code blocks specifying the "bash" language to avoid unsuitable syntax highlighting. Also includes potential improvements to the search relevance scoring, but this still needs work. |
||
#7 | 12742 | eedwards |
Add javascript from swarm.js to handle thumbnail and popup images, including adding a "Click to expand" indicator for popup images. A subsequent changelist will remove this functionality from the Swarm customizations, as this capability is now available for all guides. |
||
#6 | 12740 | eedwards |
Doc presentation style tweaks: - make the header 20% shorter. - make the guide title/version 16% smaller. - hide the left border for nested TOC entries. - lighten the hover colours for the sidebar links. - Use a cursor change to indicate that the term highlight link (the water droplet) is clickable. Also, made the "clear search" button clear the search results. |
||
#5 | 12739 | eedwards |
Add XSLT/Javascript to expose the specified language on programlisting tags and pass it on to google-code-prettify. |
||
#4 | 12738 | eedwards |
Infrastructure updates to support new Swarm DocBook documentation: - Strip out XSLTHL; it does a poor job of highlighting the kinds of syntax included in our documentation, and it does an awful job when code to be highlighted contains other markup, such as xref, link, or replaceable. - Add in google-code-prettify (from Swarm) so that generated HTML has fairly decent syntax highlighting for many of the code examples we use. Further work on syntax highlighting, particularly for PDF output, is required. - Disable TOC generation in HTML; the HTML template already includes the full TOC as a sidebar on every page. - Update the xmlns attribute and image URL filtering. - Fix a bug in HTML page title XSL that often prevented the current section title from being included. This was more obvious in the Swarm documentation which chunks into pages by section, rather than by chapter. - Update the HTML indexer to process multiple HTML files in one pass for performance, and overall simplify the code structure. - Adjust sidebar TOC activation logic to allow Bootstrap's scrollSpy code to work with section-based chunking. - Re-introduce the "brand" markup in the HTML doc heading so that Swarm's documentation can include its logo. Other guides can do so if required, but none have been updated to do so yet. - Add facility for guide-specific custom javacsript inclusion. - Add facility to apply custom CSS classes to images in documentation. This is used in Swarm documentation to provide thumbnail, popup, and "framed" images. - Adjust xref and link text to just report the chapter or section name, without the preamble text, for example: "the section called". - Add <replaceable role="bold"> to provide a second kind of emphasis to programlistings. - Added a subtle color to <filename> content, to help distinguish from other literal text. - Removed the separator rule from the first section heading while chunking by section. - Added Swarm's orderedlist styling so that each numbered item has a circle around the number and a vertical bar indicating the extend of the item. - Added a visual indicator when a link points to an external document. - Rewrote the javascript search code to search for exact and fuzzy terms matches, as well as phrases. Relevance scoring is included, but likely needs some fine tuning to suit our documentation. |
||
#3 | 12734 | eedwards |
Replace the Java-based HTML indexer with a Python-based one. The Java-based HTML indexer was based on stemming, which can reduce the size of the index notably. However, stemming producespoor search behaviour, and is language specific. For example, the word 'documentation' has a stem of 'document'. When a user searches for 'docu', 'document' is not found. Also, all punctuation was stripped, making searches for IP addresses or depot paths impossible. The Python-based indexer collects all whitespace-separated tokens. The resulting index is around 10% larger than the stemmed index, but permits much more reasonable results, particularly while the user is still typing in search terms. The Python-based indexer should be cross-platform, but has to date only been tested on Mac OSX. It should work as-is on Linux, but further work may be required on Windows or other platforms. Also, there is room for optimization, particularly when a large number of HTML documents are to be indexed. Searching multiple terms is possible, and each HTML page must match all entered terms. There is nothing fancy about handling the search terms, no conjunctions, phrase searching, etc. Depending on user feedback, we may need to add more sophistication in the future. Included in this change: - the Python indexer - removed use of Java-based indexer from common build.xml and applied the Python indexer - added additional image src filtering - better build commentary in certain targets - rewrote the client-side searching - fixed the content of the HTML <title> tags to be '<chapter> // <book>', or just '<book>' when necessary. Search results strip off the ' // <book>' to be concise. - identify results as 'pages' rather than results, as each page may contain multiple matches. - results now indicate how many matches to expect on each page. - re-enabled indexing/search for CmdRef, P4API, P4Dist, and P4Guide. |
||
#2 | 12732 | eedwards |
HTML + Javascript fix to "focus" the main content container in the new doc HTML presentation so that up/down arrow navigation works without requiring a click on the container container. |
||
#1 | 12728 | eedwards |
Upgrade ANT doc build infrastructure to assemble PDFs: - remove non-namespaced DocBook source and add namespaced DocBook source. - add Apache FOP 1.1 - copy fonts, images, XSL into _build, establishing new asset structure. The original structure remains until all guides using it can be upgraded, and several other issues can be resolved. - updated build.xml to allow for per-target build properties. - upgraded the P4SAG to use the new infrastructure. - tweaked admonition presentation in PDFs to remove admonition graphics, and resemble closely the presentation used in the new HTML layout, including the same colors. With these changes, building PDFs involves using a shell, navigating into the guide's directory (just P4SAG for now), and executing "ant pdf". Issues still to be resolved: - PDF generation encounters several warnings about missing fonts (bold versions of Symbol and ZapfDingbats), and a couple of locations where the page content exceeds the defined content area. - Due to issues within Apache FOP, PDF generation emits a substantial amount of output that is not easily suppressed without losing important warning information. - Apache FOP's interface to ANT does not expose a way to set the font base directory. The current configuration does work under Mac OSX, but further testing on Windows will need to be done to determine if the relative paths defined continue to work. The workaround is for Windows users to customize the fop-config.xml to provide absolute system paths to the required fonts. - HTML generation needs further browser testing, and exhibits broken navigation on iOS browsers within the TOC sidebar. - A number of PDF and HTML presentation tweaks still need to be made, for example: sidebars, gui* DocBook tags, whitespace, section separation, etc. |
||
//guest/perforce_software/doc_build/main/perforce/assets/webhelp/js/perforce.js | |||||
#7 | 12727 | eedwards |
Further presentation tweaks to the prototype HTML docs: - improve the presentation of the noscript warning when javascript is disabled. - customize the javascript disabled text to refer to 'documentation' instead of 'this site' - remove some stray styles and cruft that interfered with the intended presentation |
||
#6 | 12726 | eedwards |
Tweaks to the TOC presentation in prototype HTML for docs: - Animated open/close of TOC section - Reflect open/close state in TOC section expansion button - Made section nesting more visually apparent |
||
#5 | 12725 | eedwards |
Fixes for prototype HTML presentation: - don't attempt to highlight search terms when there are none - adjust inline-block styling in the header so that the text-overflow property takes effect as expected - use smaller font sizes for headings for narrower browsers, and apply transitions to heading font sizes - unroll transition styles to include all browser-specific transition styles. |
||
#4 | 12723 | eedwards |
Tweaks to search interface in HTML prototype for docs: - hookup the search field's clear button - adjust how the wordList is managed to allow for dynamic highlighting in the current page - highlight search terms in the current page |
||
#3 | 12722 | eedwards | Fix prototype HTML's javascript, to handle the implicit index.html case. | ||
#2 | 12721 | eedwards |
Tweaks to the prototype HTML presentation for docs: - use CSS classes rather than hard-coded values in javascript to implement sidebar open/close - made the search sidebar disappear when not open, not just to the right where it can be seen in wide browsers - made the TOC sidebar disappear when the search sidebar appears (at narrow browser widths), and vice-versa. |
||
#1 | 12720 | eedwards |
Apply better organization for DocBook transformation assets. In the future, other output target assets will appear as peers of webhelp, within the assets directory. |
||
//guest/perforce_software/doc_build/main/perforce/webhelp_assets/js/perforce.js | |||||
#3 | 12719 | eedwards |
Minor updates to prototype HTML presentation: - make ScrollSpy work, so that the current section of the documentation is highlighted in the sidebar TOC. - add expander buttons to the TOC so that users can manually explore the TOC. - remove the now-unused SIDR code. - remove some cruft. |
||
#2 | 12718 | eedwards |
WebHelp transformation improvements: - consolidate the Java indexer's output files into a single file to reduce HTTP requests, and tidy up the search assets the indexer created - move the search interface and results into their own pane on the right side of the display - make search incremental as users type - replace existing navigation links with a TOC sidebar that is displayed when the browser is wide enough, or use a footer with prev/next links when the browser is too narrow. In the narrow presentation, the TOC sidebar can be toggled open. - Various improvements to colours, formatting, at various browser sizes. |
||
#1 | 12717 | eedwards |
Initial prototype of new HTML generation intended to exist on the Perforce public web site. The updates features a more responsive design, removal of frames, styling improvements. More work needs to be done to ensure robust presentation across various devices, and further design tweaks may be required once various stakeholders get a chance to have a look. Future infrastructure work will need to be done: - incorporate PDF generation - some organizational tweaks to make additional output targets easy to add - HTML validation assets need to be added/incorporated |