From 186ed3937dbe06bf69806e2f82398b30d9775ed8 Mon Sep 17 00:00:00 2001 From: johha Date: Mon, 23 Feb 2026 11:50:35 +0100 Subject: [PATCH 1/5] Update docs/v3 dependencies to latest versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit updates all outdated dependencies in docs/v3 and fixes compatibility issues with Middleman 4.6+. Key changes: - middleman: 4.5.1 → 4.6.3 - middleman-livereload: 3.4.7 → 3.5.0 - thor: 1.2.2 → 1.5.0 - rack: 2.2.22 → 3.1.20 (required by Middleman 4.6) - Plus ~25 other dependency updates Technical changes: - Removed middleman-sprockets gem due to conflicts with Middleman 4.6's built-in Sass renderer - Disabled Sprockets activation in config.rb - Manually concatenated JavaScript files into all.js with proper semicolons to fix IIFE syntax errors - Pinned autoprefixer-rails to ~> 10.4.7 for stability The site now builds successfully with properly compiled CSS and working JavaScript functionality including the sidebar collapsible behavior. --- docs/v3/Gemfile | 8 +- docs/v3/Gemfile.lock | 129 +- docs/v3/config.rb | 4 +- docs/v3/source/javascripts/all.js | 3196 ++++++++++++++++++++++++++++- 4 files changed, 3259 insertions(+), 78 deletions(-) diff --git a/docs/v3/Gemfile b/docs/v3/Gemfile index 460f70cd5c..3dd9862071 100644 --- a/docs/v3/Gemfile +++ b/docs/v3/Gemfile @@ -1,12 +1,14 @@ source 'https://rubygems.org' gem 'json', '>= 2.3.0' -gem 'middleman', '~> 4.5' +gem 'middleman', '~> 4.6' gem 'middleman-autoprefixer', '~> 3.0', '>= 3.0.0' +gem 'autoprefixer-rails', '~> 10.4.7' gem 'middleman-gh-pages', '>= 0.0.3' gem 'middleman-livereload', '>= 3.4.7' -gem 'middleman-sprockets', '~> 4.1', '>= 4.1.1' -gem 'middleman-syntax', '3.6.1' +# middleman-sprockets removed - conflicts with Sass rendering in Middleman 4.6+ +# gem 'middleman-sprockets', '~> 4.1', '>= 4.1.1' +gem 'middleman-syntax', '~> 3.6' gem 'mini_racer' gem 'nokogiri', '~> 1.19.1' gem 'rake', '>= 12.3.3' diff --git a/docs/v3/Gemfile.lock b/docs/v3/Gemfile.lock index 26d05cce31..3bc3e7b34a 100644 --- a/docs/v3/Gemfile.lock +++ b/docs/v3/Gemfile.lock @@ -1,60 +1,59 @@ GEM remote: https://rubygems.org/ specs: - activesupport (7.0.10) + activesupport (8.1.2) base64 - benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) - mutex_m securerandom (>= 0.3) - tzinfo (~> 2.0) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.8) public_suffix (>= 2.0.2, < 8.0) - autoprefixer-rails (10.4.7.0) + autoprefixer-rails (10.4.21.0) execjs (~> 2) - backports (3.25.3) base64 (0.3.0) - benchmark (0.5.0) bigdecimal (4.0.1) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.12.2) concurrent-ruby (1.3.6) - contracts (0.16.1) + connection_pool (3.0.2) + contracts (0.17.3) dotenv (3.2.0) drb (2.2.3) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) - erubis (2.7.0) + erubi (1.13.1) eventmachine (1.2.7) execjs (2.10.0) fast_blank (1.0.1) fastimage (2.4.0) - ffi (1.17.3) ffi (1.17.3-arm64-darwin) ffi (1.17.3-x86_64-darwin) ffi (1.17.3-x86_64-linux-gnu) - haml (6.3.0) + haml (6.4.0) temple (>= 0.8.2) thor tilt hamster (3.0.0) concurrent-ruby (~> 1.0) - hashie (3.6.0) - http_parser.rb (0.8.0) - i18n (1.6.0) + hashie (5.1.0) + logger + http_parser.rb (0.8.1) + i18n (1.14.8) concurrent-ruby (~> 1.0) - json (2.18.0) - kramdown (2.4.0) - rexml - libv8-node (24.12.0.1) + json (2.18.1) + kramdown (2.5.2) + rexml (>= 3.4.4) libv8-node (24.12.0.1-arm64-darwin) libv8-node (24.12.0.1-x86_64-darwin) libv8-node (24.12.0.1-x86_64-linux) @@ -64,88 +63,83 @@ GEM rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) memoist (0.16.2) - middleman (4.5.1) - coffee-script (~> 2.2) - haml (>= 4.0.5) - kramdown (>= 2.3.0) - middleman-cli (= 4.5.1) - middleman-core (= 4.5.1) + middleman (4.6.3) + middleman-cli (= 4.6.3) + middleman-core (= 4.6.3) middleman-autoprefixer (3.0.0) autoprefixer-rails (~> 10.0) middleman-core (>= 4.0.0) - middleman-cli (4.5.1) - thor (>= 0.17.0, < 1.3.0) - middleman-core (4.5.1) - activesupport (>= 6.1, < 7.1) + middleman-cli (4.6.3) + thor (>= 0.17.0, < 2) + middleman-core (4.6.3) + activesupport (>= 6.1) addressable (~> 2.4) - backports (~> 3.6) - bundler (~> 2.0) - contracts (~> 0.13, < 0.17) + bundler (> 2.0) + coffee-script (~> 2.2) + contracts dotenv - erubis + erubi execjs (~> 2.0) fast_blank fastimage (~> 2.0) + haml (>= 4.0.5, < 7) hamster (~> 3.0) - hashie (~> 3.4) - i18n (~> 1.6.0) + hashie (>= 3.4, < 6.0) + i18n (>= 1.6, < 1.15) + kramdown (~> 2.4) listen (~> 3.0) memoist (~> 0.14) - padrino-helpers (~> 0.15.0) + padrino-helpers (>= 0.15, < 0.17) parallel - rack (>= 1.4.5, < 3) + rack (>= 3) + rackup sassc (~> 2.0) servolux - tilt (~> 2.0.9) + tilt (~> 2.2) toml - uglifier (~> 3.0) + uglifier (>= 3, < 5) webrick middleman-gh-pages (0.4.1) rake (> 0.9.3) - middleman-livereload (3.4.7) + middleman-livereload (3.5.0) em-websocket (~> 0.5.1) middleman-core (>= 3.3) - rack-livereload (~> 0.3.15) - middleman-sprockets (4.1.1) - middleman-core (~> 4.0) - sprockets (>= 3.0) + rack-livereload (~> 0.6.1) middleman-syntax (3.6.1) middleman-core (>= 3.2) rouge (~> 3.2) - mini_portile2 (2.8.9) mini_racer (0.19.2) libv8-node (~> 24.12.0.1) minitest (6.0.1) prism (~> 1.5) mutex_m (0.3.0) - nokogiri (1.19.1) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) nokogiri (1.19.1-arm64-darwin) racc (~> 1.4) nokogiri (1.19.1-x86_64-darwin) racc (~> 1.4) nokogiri (1.19.1-x86_64-linux-gnu) racc (~> 1.4) - padrino-helpers (0.15.3) + padrino-helpers (0.16.0) i18n (>= 0.6.7, < 2) - padrino-support (= 0.15.3) - tilt (>= 1.4.1, < 3) - padrino-support (0.15.3) + padrino-support (= 0.16.0) + tilt (>= 2.1, < 3) + padrino-support (0.16.0) parallel (1.27.0) parslet (2.0.0) - prism (1.8.0) + prism (1.9.0) public_suffix (7.0.2) racc (1.8.1) - rack (2.2.22) - rack-livereload (0.3.17) - rack + rack (3.1.20) + rack-livereload (0.6.1) + rack (>= 3.0, < 3.2) + rackup (2.3.1) + rack (>= 3) rake (13.3.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) redcarpet (3.6.1) - rexml (3.4.2) + rexml (3.4.4) rouge (3.30.0) sass (3.7.4) sass-listen (~> 4.0.0) @@ -156,34 +150,31 @@ GEM ffi (~> 1.9) securerandom (0.4.1) servolux (0.13.0) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - temple (0.10.3) - thor (1.2.2) - tilt (2.0.11) + temple (0.10.4) + thor (1.5.0) + tilt (2.7.0) toml (0.3.0) parslet (>= 1.8.0, < 3.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - uglifier (3.2.0) + uglifier (4.2.1) execjs (>= 0.3.0, < 3) + uri (1.1.1) webrick (1.9.2) PLATFORMS arm64-darwin - ruby x86_64-darwin x86_64-linux DEPENDENCIES + autoprefixer-rails (~> 10.4.7) json (>= 2.3.0) - middleman (~> 4.5) + middleman (~> 4.6) middleman-autoprefixer (~> 3.0, >= 3.0.0) middleman-gh-pages (>= 0.0.3) middleman-livereload (>= 3.4.7) - middleman-sprockets (~> 4.1, >= 4.1.1) - middleman-syntax (= 3.6.1) + middleman-syntax (~> 3.6) mini_racer nokogiri (~> 1.19.1) rake (>= 12.3.3) @@ -191,4 +182,4 @@ DEPENDENCIES sass BUNDLED WITH - 2.4.19 + 2.6.9 diff --git a/docs/v3/config.rb b/docs/v3/config.rb index 6ca7f265c5..7f054a2464 100644 --- a/docs/v3/config.rb +++ b/docs/v3/config.rb @@ -20,7 +20,9 @@ # Activate the syntax highlighter activate :syntax -activate :sprockets +# Sprockets removed - caused conflicts with Sass rendering +# We'll handle JS concatenation differently +# activate :sprockets activate :autoprefixer do |config| config.browsers = ['last 2 version', 'Firefox ESR'] diff --git a/docs/v3/source/javascripts/all.js b/docs/v3/source/javascripts/all.js index 1643c0178f..688d92ee75 100644 --- a/docs/v3/source/javascripts/all.js +++ b/docs/v3/source/javascripts/all.js @@ -1,5 +1,3191 @@ -//= require ./lib/_energize -//= require ./lib/_dropdown -//= require ./app/_search -//= require ./app/_toc -//= require ./app/_version_dropdown +// Concatenated JavaScript file for docs/v3 +// This file was created because middleman-sprockets conflicts with Middleman 4.6+ Sass rendering +// +// To regenerate this file, run from docs/v3 directory: +// cat source/javascripts/lib/_energize.js > source/javascripts/all.js && \ +// echo ";" >> source/javascripts/all.js && \ +// cat source/javascripts/lib/_dropdown.js >> source/javascripts/all.js && \ +// echo ";" >> source/javascripts/all.js && \ +// cat source/javascripts/lib/_lunr.js >> source/javascripts/all.js && \ +// echo ";" >> source/javascripts/all.js && \ +// tail -n +2 source/javascripts/app/_search.js >> source/javascripts/all.js && \ +// echo ";" >> source/javascripts/all.js && \ +// cat source/javascripts/lib/_jquery_ui.js >> source/javascripts/all.js && \ +// echo ";" >> source/javascripts/all.js && \ +// cat source/javascripts/lib/_jquery.tocify.js >> source/javascripts/all.js && \ +// echo ";" >> source/javascripts/all.js && \ +// cat source/javascripts/lib/_imagesloaded.min.js >> source/javascripts/all.js && \ +// echo ";" >> source/javascripts/all.js && \ +// tail -n +4 source/javascripts/app/_toc.js >> source/javascripts/all.js && \ +// echo ";" >> source/javascripts/all.js && \ +// cat source/javascripts/app/_version_dropdown.js >> source/javascripts/all.js + +/** + * energize.js v0.1.0 + * + * Speeds up click events on mobile devices. + * https://github.com/davidcalhoun/energize.js + */ + +(function() { // Sandbox + /** + * Don't add to non-touch devices, which don't need to be sped up + */ + if(!('ontouchstart' in window)) return; + + var lastClick = {}, + isThresholdReached, touchstart, touchmove, touchend, + click, closest; + + /** + * isThresholdReached + * + * Compare touchstart with touchend xy coordinates, + * and only fire simulated click event if the coordinates + * are nearby. (don't want clicking to be confused with a swipe) + */ + isThresholdReached = function(startXY, xy) { + return Math.abs(startXY[0] - xy[0]) > 5 || Math.abs(startXY[1] - xy[1]) > 5; + }; + + /** + * touchstart + * + * Save xy coordinates when the user starts touching the screen + */ + touchstart = function(e) { + this.startXY = [e.touches[0].clientX, e.touches[0].clientY]; + this.threshold = false; + }; + + /** + * touchmove + * + * Check if the user is scrolling past the threshold. + * Have to check here because touchend will not always fire + * on some tested devices (Kindle Fire?) + */ + touchmove = function(e) { + // NOOP if the threshold has already been reached + if(this.threshold) return false; + + this.threshold = isThresholdReached(this.startXY, [e.touches[0].clientX, e.touches[0].clientY]); + }; + + /** + * touchend + * + * If the user didn't scroll past the threshold between + * touchstart and touchend, fire a simulated click. + * + * (This will fire before a native click) + */ + touchend = function(e) { + // Don't fire a click if the user scrolled past the threshold + if(this.threshold || isThresholdReached(this.startXY, [e.changedTouches[0].clientX, e.changedTouches[0].clientY])) { + return; + } + + /** + * Create and fire a click event on the target element + * https://developer.mozilla.org/en/DOM/event.initMouseEvent + */ + var touch = e.changedTouches[0], + evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); + evt.simulated = true; // distinguish from a normal (nonsimulated) click + e.target.dispatchEvent(evt); + }; + + /** + * click + * + * Because we've already fired a click event in touchend, + * we need to listed for all native click events here + * and suppress them as necessary. + */ + click = function(e) { + /** + * Prevent ghost clicks by only allowing clicks we created + * in the click event we fired (look for e.simulated) + */ + var time = Date.now(), + timeDiff = time - lastClick.time, + x = e.clientX, + y = e.clientY, + xyDiff = [Math.abs(lastClick.x - x), Math.abs(lastClick.y - y)], + target = closest(e.target, 'A') || e.target, // needed for standalone apps + nodeName = target.nodeName, + isLink = nodeName === 'A', + standAlone = window.navigator.standalone && isLink && e.target.getAttribute("href"); + + lastClick.time = time; + lastClick.x = x; + lastClick.y = y; + + /** + * Unfortunately Android sometimes fires click events without touch events (seen on Kindle Fire), + * so we have to add more logic to determine the time of the last click. Not perfect... + * + * Older, simpler check: if((!e.simulated) || standAlone) + */ + if((!e.simulated && (timeDiff < 500 || (timeDiff < 1500 && xyDiff[0] < 50 && xyDiff[1] < 50))) || standAlone) { + e.preventDefault(); + e.stopPropagation(); + if(!standAlone) return false; + } + + /** + * Special logic for standalone web apps + * See http://stackoverflow.com/questions/2898740/iphone-safari-web-app-opens-links-in-new-window + */ + if(standAlone) { + window.location = target.getAttribute("href"); + } + + /** + * Add an energize-focus class to the targeted link (mimics :focus behavior) + * TODO: test and/or remove? Does this work? + */ + if(!target || !target.classList) return; + target.classList.add("energize-focus"); + window.setTimeout(function(){ + target.classList.remove("energize-focus"); + }, 150); + }; + + /** + * closest + * @param {HTMLElement} node current node to start searching from. + * @param {string} tagName the (uppercase) name of the tag you're looking for. + * + * Find the closest ancestor tag of a given node. + * + * Starts at node and goes up the DOM tree looking for a + * matching nodeName, continuing until hitting document.body + */ + closest = function(node, tagName){ + var curNode = node; + + while(curNode !== document.body) { // go up the dom until we find the tag we're after + if(!curNode || curNode.nodeName === tagName) { return curNode; } // found + curNode = curNode.parentNode; // not found, so keep going up + } + + return null; // not found + }; + + /** + * Add all delegated event listeners + * + * All the events we care about bubble up to document, + * so we can take advantage of event delegation. + * + * Note: no need to wait for DOMContentLoaded here + */ + document.addEventListener('touchstart', touchstart, false); + document.addEventListener('touchmove', touchmove, false); + document.addEventListener('touchend', touchend, false); + document.addEventListener('click', click, true); // TODO: why does this use capture? + +})();; +/* ======================================================================== + * Bootstrap: dropdown.js v3.3.6 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle="dropdown"]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.VERSION = '3.3.6' + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + function clearMenus(e) { + if (e && e.which === 3) return + $(backdrop).remove() + $(toggle).each(function () { + var $this = $(this) + var $parent = getParent($this) + var relatedTarget = { relatedTarget: this } + + if (!$parent.hasClass('open')) return + + if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return + + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this.attr('aria-expanded', 'false') + $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) + }) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $(document.createElement('div')) + .addClass('dropdown-backdrop') + .insertAfter($(this)) + .on('click', clearMenus) + } + + var relatedTarget = { relatedTarget: this } + $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this + .trigger('focus') + .attr('aria-expanded', 'true') + + $parent + .toggleClass('open') + .trigger($.Event('shown.bs.dropdown', relatedTarget)) + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive && e.which != 27 || isActive && e.which == 27) { + if (e.which == 27) $parent.find(toggle).trigger('focus') + return $this.trigger('click') + } + + var desc = ' li:not(.disabled):visible a' + var $items = $parent.find('.dropdown-menu' + desc) + + if (!$items.length) return + + var index = $items.index(e.target) + + if (e.which == 38 && index > 0) index-- // up + if (e.which == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items.eq(index).trigger('focus') + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.dropdown') + + if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.dropdown + + $.fn.dropdown = Plugin + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) + .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) + +}(jQuery); +; +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 0.5.7 + * Copyright (C) 2014 Oliver Nightingale + * MIT Licensed + * @license + */ + +(function(){ + + /** + * Convenience function for instantiating a new lunr index and configuring it + * with the default pipeline functions and the passed config function. + * + * When using this convenience function a new index will be created with the + * following functions already in the pipeline: + * + * lunr.StopWordFilter - filters out any stop words before they enter the + * index + * + * lunr.stemmer - stems the tokens before entering the index. + * + * Example: + * + * var idx = lunr(function () { + * this.field('title', 10) + * this.field('tags', 100) + * this.field('body') + * + * this.ref('cid') + * + * this.pipeline.add(function () { + * // some custom pipeline function + * }) + * + * }) + * + * @param {Function} config A function that will be called with the new instance + * of the lunr.Index as both its context and first parameter. It can be used to + * customize the instance of new lunr.Index. + * @namespace + * @module + * @returns {lunr.Index} + * + */ + var lunr = function (config) { + var idx = new lunr.Index + + idx.pipeline.add( + lunr.trimmer, + lunr.stopWordFilter, + lunr.stemmer + ) + + if (config) config.call(idx, idx) + + return idx + } + + lunr.version = "0.5.7" + /*! + * lunr.utils + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * A namespace containing utils for the rest of the lunr library + */ + lunr.utils = {} + + /** + * Print a warning message to the console. + * + * @param {String} message The message to be printed. + * @memberOf Utils + */ + lunr.utils.warn = (function (global) { + return function (message) { + if (global.console && console.warn) { + console.warn(message) + } + } + })(this) + + /*! + * lunr.EventEmitter + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.EventEmitter is an event emitter for lunr. It manages adding and removing event handlers and triggering events and their handlers. + * + * @constructor + */ + lunr.EventEmitter = function () { + this.events = {} + } + + /** + * Binds a handler function to a specific event(s). + * + * Can bind a single function to many different events in one call. + * + * @param {String} [eventName] The name(s) of events to bind this function to. + * @param {Function} handler The function to call when an event is fired. + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.addListener = function () { + var args = Array.prototype.slice.call(arguments), + fn = args.pop(), + names = args + + if (typeof fn !== "function") throw new TypeError ("last argument must be a function") + + names.forEach(function (name) { + if (!this.hasHandler(name)) this.events[name] = [] + this.events[name].push(fn) + }, this) + } + + /** + * Removes a handler function from a specific event. + * + * @param {String} eventName The name of the event to remove this function from. + * @param {Function} handler The function to remove from an event. + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.removeListener = function (name, fn) { + if (!this.hasHandler(name)) return + + var fnIndex = this.events[name].indexOf(fn) + this.events[name].splice(fnIndex, 1) + + if (!this.events[name].length) delete this.events[name] + } + + /** + * Calls all functions bound to the given event. + * + * Additional data can be passed to the event handler as arguments to `emit` + * after the event name. + * + * @param {String} eventName The name of the event to emit. + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.emit = function (name) { + if (!this.hasHandler(name)) return + + var args = Array.prototype.slice.call(arguments, 1) + + this.events[name].forEach(function (fn) { + fn.apply(undefined, args) + }) + } + + /** + * Checks whether a handler has ever been stored against an event. + * + * @param {String} eventName The name of the event to check. + * @private + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.hasHandler = function (name) { + return name in this.events + } + + /*! + * lunr.tokenizer + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * A function for splitting a string into tokens ready to be inserted into + * the search index. + * + * @module + * @param {String} obj The string to convert into tokens + * @returns {Array} + */ + lunr.tokenizer = function (obj) { + if (!arguments.length || obj == null || obj == undefined) return [] + if (Array.isArray(obj)) return obj.map(function (t) { return t.toLowerCase() }) + + var str = obj.toString().replace(/^\s+/, '') + + for (var i = str.length - 1; i >= 0; i--) { + if (/\S/.test(str.charAt(i))) { + str = str.substring(0, i + 1) + break + } + } + + return str + .split(/(?:\s+|\-)/) + .filter(function (token) { + return !!token + }) + .map(function (token) { + return token.toLowerCase() + }) + } + /*! + * lunr.Pipeline + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ + lunr.Pipeline = function () { + this._stack = [] + } + + lunr.Pipeline.registeredFunctions = {} + + /** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {Function} fn The function to check for. + * @param {String} label The label to register this function with + * @memberOf Pipeline + */ + lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn + } + + /** + * Warns if the function is not registered as a Pipeline function. + * + * @param {Function} fn The function to check for. + * @private + * @memberOf Pipeline + */ + lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } + } + + /** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised The serialised pipeline to load. + * @returns {lunr.Pipeline} + * @memberOf Pipeline + */ + lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error ('Cannot load un-registered function: ' + fnName) + } + }) + + return pipeline + } + + /** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} functions Any number of functions to add to the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) + } + + /** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} existingFn A function that already exists in the pipeline. + * @param {Function} newFn The new function to add to the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + 1 + this._stack.splice(pos, 0, newFn) + } + + /** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} existingFn A function that already exists in the pipeline. + * @param {Function} newFn The new function to add to the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + this._stack.splice(pos, 0, newFn) + } + + /** + * Removes a function from the pipeline. + * + * @param {Function} fn The function to remove from the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + this._stack.splice(pos, 1) + } + + /** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.run = function (tokens) { + var out = [], + tokenLength = tokens.length, + stackLength = this._stack.length + + for (var i = 0; i < tokenLength; i++) { + var token = tokens[i] + + for (var j = 0; j < stackLength; j++) { + token = this._stack[j](token, i, tokens) + if (token === void 0) break + }; + + if (token !== void 0) out.push(token) + }; + + return out + } + + /** + * Resets the pipeline by removing any existing processors. + * + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.reset = function () { + this._stack = [] + } + + /** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) + } + /*! + * lunr.Vector + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Vectors implement vector related operations for + * a series of elements. + * + * @constructor + */ + lunr.Vector = function () { + this._magnitude = null + this.list = undefined + this.length = 0 + } + + /** + * lunr.Vector.Node is a simple struct for each node + * in a lunr.Vector. + * + * @private + * @param {Number} The index of the node in the vector. + * @param {Object} The data at this node in the vector. + * @param {lunr.Vector.Node} The node directly after this node in the vector. + * @constructor + * @memberOf Vector + */ + lunr.Vector.Node = function (idx, val, next) { + this.idx = idx + this.val = val + this.next = next + } + + /** + * Inserts a new value at a position in a vector. + * + * @param {Number} The index at which to insert a value. + * @param {Object} The object to insert in the vector. + * @memberOf Vector. + */ + lunr.Vector.prototype.insert = function (idx, val) { + var list = this.list + + if (!list) { + this.list = new lunr.Vector.Node (idx, val, list) + return this.length++ + } + + var prev = list, + next = list.next + + while (next != undefined) { + if (idx < next.idx) { + prev.next = new lunr.Vector.Node (idx, val, next) + return this.length++ + } + + prev = next, next = next.next + } + + prev.next = new lunr.Vector.Node (idx, val, next) + return this.length++ + } + + /** + * Calculates the magnitude of this vector. + * + * @returns {Number} + * @memberOf Vector + */ + lunr.Vector.prototype.magnitude = function () { + if (this._magniture) return this._magnitude + var node = this.list, + sumOfSquares = 0, + val + + while (node) { + val = node.val + sumOfSquares += val * val + node = node.next + } + + return this._magnitude = Math.sqrt(sumOfSquares) + } + + /** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector The vector to compute the dot product with. + * @returns {Number} + * @memberOf Vector + */ + lunr.Vector.prototype.dot = function (otherVector) { + var node = this.list, + otherNode = otherVector.list, + dotProduct = 0 + + while (node && otherNode) { + if (node.idx < otherNode.idx) { + node = node.next + } else if (node.idx > otherNode.idx) { + otherNode = otherNode.next + } else { + dotProduct += node.val * otherNode.val + node = node.next + otherNode = otherNode.next + } + } + + return dotProduct + } + + /** + * Calculates the cosine similarity between this vector and another + * vector. + * + * @param {lunr.Vector} otherVector The other vector to calculate the + * similarity with. + * @returns {Number} + * @memberOf Vector + */ + lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / (this.magnitude() * otherVector.magnitude()) + } + /*! + * lunr.SortedSet + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.SortedSets are used to maintain an array of uniq values in a sorted + * order. + * + * @constructor + */ + lunr.SortedSet = function () { + this.length = 0 + this.elements = [] + } + + /** + * Loads a previously serialised sorted set. + * + * @param {Array} serialisedData The serialised set to load. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.load = function (serialisedData) { + var set = new this + + set.elements = serialisedData + set.length = serialisedData.length + + return set + } + + /** + * Inserts new items into the set in the correct position to maintain the + * order. + * + * @param {Object} The objects to add to this set. + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.add = function () { + Array.prototype.slice.call(arguments).forEach(function (element) { + if (~this.indexOf(element)) return + this.elements.splice(this.locationFor(element), 0, element) + }, this) + + this.length = this.elements.length + } + + /** + * Converts this sorted set into an array. + * + * @returns {Array} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.toArray = function () { + return this.elements.slice() + } + + /** + * Creates a new array with the results of calling a provided function on every + * element in this sorted set. + * + * Delegates to Array.prototype.map and has the same signature. + * + * @param {Function} fn The function that is called on each element of the + * set. + * @param {Object} ctx An optional object that can be used as the context + * for the function fn. + * @returns {Array} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.map = function (fn, ctx) { + return this.elements.map(fn, ctx) + } + + /** + * Executes a provided function once per sorted set element. + * + * Delegates to Array.prototype.forEach and has the same signature. + * + * @param {Function} fn The function that is called on each element of the + * set. + * @param {Object} ctx An optional object that can be used as the context + * @memberOf SortedSet + * for the function fn. + */ + lunr.SortedSet.prototype.forEach = function (fn, ctx) { + return this.elements.forEach(fn, ctx) + } + + /** + * Returns the index at which a given element can be found in the + * sorted set, or -1 if it is not present. + * + * @param {Object} elem The object to locate in the sorted set. + * @param {Number} start An optional index at which to start searching from + * within the set. + * @param {Number} end An optional index at which to stop search from within + * the set. + * @returns {Number} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.indexOf = function (elem, start, end) { + var start = start || 0, + end = end || this.elements.length, + sectionLength = end - start, + pivot = start + Math.floor(sectionLength / 2), + pivotElem = this.elements[pivot] + + if (sectionLength <= 1) { + if (pivotElem === elem) { + return pivot + } else { + return -1 + } + } + + if (pivotElem < elem) return this.indexOf(elem, pivot, end) + if (pivotElem > elem) return this.indexOf(elem, start, pivot) + if (pivotElem === elem) return pivot + } + + /** + * Returns the position within the sorted set that an element should be + * inserted at to maintain the current order of the set. + * + * This function assumes that the element to search for does not already exist + * in the sorted set. + * + * @param {Object} elem The elem to find the position for in the set + * @param {Number} start An optional index at which to start searching from + * within the set. + * @param {Number} end An optional index at which to stop search from within + * the set. + * @returns {Number} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.locationFor = function (elem, start, end) { + var start = start || 0, + end = end || this.elements.length, + sectionLength = end - start, + pivot = start + Math.floor(sectionLength / 2), + pivotElem = this.elements[pivot] + + if (sectionLength <= 1) { + if (pivotElem > elem) return pivot + if (pivotElem < elem) return pivot + 1 + } + + if (pivotElem < elem) return this.locationFor(elem, pivot, end) + if (pivotElem > elem) return this.locationFor(elem, start, pivot) + } + + /** + * Creates a new lunr.SortedSet that contains the elements in the intersection + * of this set and the passed set. + * + * @param {lunr.SortedSet} otherSet The set to intersect with this set. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.intersect = function (otherSet) { + var intersectSet = new lunr.SortedSet, + i = 0, j = 0, + a_len = this.length, b_len = otherSet.length, + a = this.elements, b = otherSet.elements + + while (true) { + if (i > a_len - 1 || j > b_len - 1) break + + if (a[i] === b[j]) { + intersectSet.add(a[i]) + i++, j++ + continue + } + + if (a[i] < b[j]) { + i++ + continue + } + + if (a[i] > b[j]) { + j++ + continue + } + }; + + return intersectSet + } + + /** + * Makes a copy of this set + * + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.clone = function () { + var clone = new lunr.SortedSet + + clone.elements = this.toArray() + clone.length = clone.elements.length + + return clone + } + + /** + * Creates a new lunr.SortedSet that contains the elements in the union + * of this set and the passed set. + * + * @param {lunr.SortedSet} otherSet The set to union with this set. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.union = function (otherSet) { + var longSet, shortSet, unionSet + + if (this.length >= otherSet.length) { + longSet = this, shortSet = otherSet + } else { + longSet = otherSet, shortSet = this + } + + unionSet = longSet.clone() + + unionSet.add.apply(unionSet, shortSet.toArray()) + + return unionSet + } + + /** + * Returns a representation of the sorted set ready for serialisation. + * + * @returns {Array} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.toJSON = function () { + return this.toArray() + } + /*! + * lunr.Index + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Index is object that manages a search index. It contains the indexes + * and stores all the tokens and document lookups. It also provides the main + * user facing API for the library. + * + * @constructor + */ + lunr.Index = function () { + this._fields = [] + this._ref = 'id' + this.pipeline = new lunr.Pipeline + this.documentStore = new lunr.Store + this.tokenStore = new lunr.TokenStore + this.corpusTokens = new lunr.SortedSet + this.eventEmitter = new lunr.EventEmitter + + this._idfCache = {} + + this.on('add', 'remove', 'update', (function () { + this._idfCache = {} + }).bind(this)) + } + + /** + * Bind a handler to events being emitted by the index. + * + * The handler can be bound to many events at the same time. + * + * @param {String} [eventName] The name(s) of events to bind the function to. + * @param {Function} handler The serialised set to load. + * @memberOf Index + */ + lunr.Index.prototype.on = function () { + var args = Array.prototype.slice.call(arguments) + return this.eventEmitter.addListener.apply(this.eventEmitter, args) + } + + /** + * Removes a handler from an event being emitted by the index. + * + * @param {String} eventName The name of events to remove the function from. + * @param {Function} handler The serialised set to load. + * @memberOf Index + */ + lunr.Index.prototype.off = function (name, fn) { + return this.eventEmitter.removeListener(name, fn) + } + + /** + * Loads a previously serialised index. + * + * Issues a warning if the index being imported was serialised + * by a different version of lunr. + * + * @param {Object} serialisedData The serialised set to load. + * @returns {lunr.Index} + * @memberOf Index + */ + lunr.Index.load = function (serialisedData) { + if (serialisedData.version !== lunr.version) { + lunr.utils.warn('version mismatch: current ' + lunr.version + ' importing ' + serialisedData.version) + } + + var idx = new this + + idx._fields = serialisedData.fields + idx._ref = serialisedData.ref + + idx.documentStore = lunr.Store.load(serialisedData.documentStore) + idx.tokenStore = lunr.TokenStore.load(serialisedData.tokenStore) + idx.corpusTokens = lunr.SortedSet.load(serialisedData.corpusTokens) + idx.pipeline = lunr.Pipeline.load(serialisedData.pipeline) + + return idx + } + + /** + * Adds a field to the list of fields that will be searchable within documents + * in the index. + * + * An optional boost param can be passed to affect how much tokens in this field + * rank in search results, by default the boost value is 1. + * + * Fields should be added before any documents are added to the index, fields + * that are added after documents are added to the index will only apply to new + * documents added to the index. + * + * @param {String} fieldName The name of the field within the document that + * should be indexed + * @param {Number} boost An optional boost that can be applied to terms in this + * field. + * @returns {lunr.Index} + * @memberOf Index + */ + lunr.Index.prototype.field = function (fieldName, opts) { + var opts = opts || {}, + field = { name: fieldName, boost: opts.boost || 1 } + + this._fields.push(field) + return this + } + + /** + * Sets the property used to uniquely identify documents added to the index, + * by default this property is 'id'. + * + * This should only be changed before adding documents to the index, changing + * the ref property without resetting the index can lead to unexpected results. + * + * @param {String} refName The property to use to uniquely identify the + * documents in the index. + * @param {Boolean} emitEvent Whether to emit add events, defaults to true + * @returns {lunr.Index} + * @memberOf Index + */ + lunr.Index.prototype.ref = function (refName) { + this._ref = refName + return this + } + + /** + * Add a document to the index. + * + * This is the way new documents enter the index, this function will run the + * fields from the document through the index's pipeline and then add it to + * the index, it will then show up in search results. + * + * An 'add' event is emitted with the document that has been added and the index + * the document has been added to. This event can be silenced by passing false + * as the second argument to add. + * + * @param {Object} doc The document to add to the index. + * @param {Boolean} emitEvent Whether or not to emit events, default true. + * @memberOf Index + */ + lunr.Index.prototype.add = function (doc, emitEvent) { + var docTokens = {}, + allDocumentTokens = new lunr.SortedSet, + docRef = doc[this._ref], + emitEvent = emitEvent === undefined ? true : emitEvent + + this._fields.forEach(function (field) { + var fieldTokens = this.pipeline.run(lunr.tokenizer(doc[field.name])) + + docTokens[field.name] = fieldTokens + lunr.SortedSet.prototype.add.apply(allDocumentTokens, fieldTokens) + }, this) + + this.documentStore.set(docRef, allDocumentTokens) + lunr.SortedSet.prototype.add.apply(this.corpusTokens, allDocumentTokens.toArray()) + + for (var i = 0; i < allDocumentTokens.length; i++) { + var token = allDocumentTokens.elements[i] + var tf = this._fields.reduce(function (memo, field) { + var fieldLength = docTokens[field.name].length + + if (!fieldLength) return memo + + var tokenCount = docTokens[field.name].filter(function (t) { return t === token }).length + + return memo + (tokenCount / fieldLength * field.boost) + }, 0) + + this.tokenStore.add(token, { ref: docRef, tf: tf }) + }; + + if (emitEvent) this.eventEmitter.emit('add', doc, this) + } + + /** + * Removes a document from the index. + * + * To make sure documents no longer show up in search results they can be + * removed from the index using this method. + * + * The document passed only needs to have the same ref property value as the + * document that was added to the index, they could be completely different + * objects. + * + * A 'remove' event is emitted with the document that has been removed and the index + * the document has been removed from. This event can be silenced by passing false + * as the second argument to remove. + * + * @param {Object} doc The document to remove from the index. + * @param {Boolean} emitEvent Whether to emit remove events, defaults to true + * @memberOf Index + */ + lunr.Index.prototype.remove = function (doc, emitEvent) { + var docRef = doc[this._ref], + emitEvent = emitEvent === undefined ? true : emitEvent + + if (!this.documentStore.has(docRef)) return + + var docTokens = this.documentStore.get(docRef) + + this.documentStore.remove(docRef) + + docTokens.forEach(function (token) { + this.tokenStore.remove(token, docRef) + }, this) + + if (emitEvent) this.eventEmitter.emit('remove', doc, this) + } + + /** + * Updates a document in the index. + * + * When a document contained within the index gets updated, fields changed, + * added or removed, to make sure it correctly matched against search queries, + * it should be updated in the index. + * + * This method is just a wrapper around `remove` and `add` + * + * An 'update' event is emitted with the document that has been updated and the index. + * This event can be silenced by passing false as the second argument to update. Only + * an update event will be fired, the 'add' and 'remove' events of the underlying calls + * are silenced. + * + * @param {Object} doc The document to update in the index. + * @param {Boolean} emitEvent Whether to emit update events, defaults to true + * @see Index.prototype.remove + * @see Index.prototype.add + * @memberOf Index + */ + lunr.Index.prototype.update = function (doc, emitEvent) { + var emitEvent = emitEvent === undefined ? true : emitEvent + + this.remove(doc, false) + this.add(doc, false) + + if (emitEvent) this.eventEmitter.emit('update', doc, this) + } + + /** + * Calculates the inverse document frequency for a token within the index. + * + * @param {String} token The token to calculate the idf of. + * @see Index.prototype.idf + * @private + * @memberOf Index + */ + lunr.Index.prototype.idf = function (term) { + var cacheKey = "@" + term + if (Object.prototype.hasOwnProperty.call(this._idfCache, cacheKey)) return this._idfCache[cacheKey] + + var documentFrequency = this.tokenStore.count(term), + idf = 1 + + if (documentFrequency > 0) { + idf = 1 + Math.log(this.tokenStore.length / documentFrequency) + } + + return this._idfCache[cacheKey] = idf + } + + /** + * Searches the index using the passed query. + * + * Queries should be a string, multiple words are allowed and will lead to an + * AND based query, e.g. `idx.search('foo bar')` will run a search for + * documents containing both 'foo' and 'bar'. + * + * All query tokens are passed through the same pipeline that document tokens + * are passed through, so any language processing involved will be run on every + * query term. + * + * Each query term is expanded, so that the term 'he' might be expanded to + * 'hello' and 'help' if those terms were already included in the index. + * + * Matching documents are returned as an array of objects, each object contains + * the matching document ref, as set for this index, and the similarity score + * for this document against the query. + * + * @param {String} query The query to search the index with. + * @returns {Object} + * @see Index.prototype.idf + * @see Index.prototype.documentVector + * @memberOf Index + */ + lunr.Index.prototype.search = function (query) { + var queryTokens = this.pipeline.run(lunr.tokenizer(query)), + queryVector = new lunr.Vector, + documentSets = [], + fieldBoosts = this._fields.reduce(function (memo, f) { return memo + f.boost }, 0) + + var hasSomeToken = queryTokens.some(function (token) { + return this.tokenStore.has(token) + }, this) + + if (!hasSomeToken) return [] + + queryTokens + .forEach(function (token, i, tokens) { + var tf = 1 / tokens.length * this._fields.length * fieldBoosts, + self = this + + var set = this.tokenStore.expand(token).reduce(function (memo, key) { + var pos = self.corpusTokens.indexOf(key), + idf = self.idf(key), + similarityBoost = 1, + set = new lunr.SortedSet + + // if the expanded key is not an exact match to the token then + // penalise the score for this key by how different the key is + // to the token. + if (key !== token) { + var diff = Math.max(3, key.length - token.length) + similarityBoost = 1 / Math.log(diff) + } + + // calculate the query tf-idf score for this token + // applying an similarityBoost to ensure exact matches + // these rank higher than expanded terms + if (pos > -1) queryVector.insert(pos, tf * idf * similarityBoost) + + // add all the documents that have this key into a set + Object.keys(self.tokenStore.get(key)).forEach(function (ref) { set.add(ref) }) + + return memo.union(set) + }, new lunr.SortedSet) + + documentSets.push(set) + }, this) + + var documentSet = documentSets.reduce(function (memo, set) { + return memo.intersect(set) + }) + + return documentSet + .map(function (ref) { + return { ref: ref, score: queryVector.similarity(this.documentVector(ref)) } + }, this) + .sort(function (a, b) { + return b.score - a.score + }) + } + + /** + * Generates a vector containing all the tokens in the document matching the + * passed documentRef. + * + * The vector contains the tf-idf score for each token contained in the + * document with the passed documentRef. The vector will contain an element + * for every token in the indexes corpus, if the document does not contain that + * token the element will be 0. + * + * @param {Object} documentRef The ref to find the document with. + * @returns {lunr.Vector} + * @private + * @memberOf Index + */ + lunr.Index.prototype.documentVector = function (documentRef) { + var documentTokens = this.documentStore.get(documentRef), + documentTokensLength = documentTokens.length, + documentVector = new lunr.Vector + + for (var i = 0; i < documentTokensLength; i++) { + var token = documentTokens.elements[i], + tf = this.tokenStore.get(token)[documentRef].tf, + idf = this.idf(token) + + documentVector.insert(this.corpusTokens.indexOf(token), tf * idf) + }; + + return documentVector + } + + /** + * Returns a representation of the index ready for serialisation. + * + * @returns {Object} + * @memberOf Index + */ + lunr.Index.prototype.toJSON = function () { + return { + version: lunr.version, + fields: this._fields, + ref: this._ref, + documentStore: this.documentStore.toJSON(), + tokenStore: this.tokenStore.toJSON(), + corpusTokens: this.corpusTokens.toJSON(), + pipeline: this.pipeline.toJSON() + } + } + + /** + * Applies a plugin to the current index. + * + * A plugin is a function that is called with the index as its context. + * Plugins can be used to customise or extend the behaviour the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied to the index. + * + * The plugin function will be called with the index as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index as its context. + * + * Example: + * + * var myPlugin = function (idx, arg1, arg2) { + * // `this` is the index to be extended + * // apply any extensions etc here. + * } + * + * var idx = lunr(function () { + * this.use(myPlugin, 'arg1', 'arg2') + * }) + * + * @param {Function} plugin The plugin to apply. + * @memberOf Index + */ + lunr.Index.prototype.use = function (plugin) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + plugin.apply(this, args) + } + /*! + * lunr.Store + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Store is a simple key-value store used for storing sets of tokens for + * documents stored in index. + * + * @constructor + * @module + */ + lunr.Store = function () { + this.store = {} + this.length = 0 + } + + /** + * Loads a previously serialised store + * + * @param {Object} serialisedData The serialised store to load. + * @returns {lunr.Store} + * @memberOf Store + */ + lunr.Store.load = function (serialisedData) { + var store = new this + + store.length = serialisedData.length + store.store = Object.keys(serialisedData.store).reduce(function (memo, key) { + memo[key] = lunr.SortedSet.load(serialisedData.store[key]) + return memo + }, {}) + + return store + } + + /** + * Stores the given tokens in the store against the given id. + * + * @param {Object} id The key used to store the tokens against. + * @param {Object} tokens The tokens to store against the key. + * @memberOf Store + */ + lunr.Store.prototype.set = function (id, tokens) { + if (!this.has(id)) this.length++ + this.store[id] = tokens + } + + /** + * Retrieves the tokens from the store for a given key. + * + * @param {Object} id The key to lookup and retrieve from the store. + * @returns {Object} + * @memberOf Store + */ + lunr.Store.prototype.get = function (id) { + return this.store[id] + } + + /** + * Checks whether the store contains a key. + * + * @param {Object} id The id to look up in the store. + * @returns {Boolean} + * @memberOf Store + */ + lunr.Store.prototype.has = function (id) { + return id in this.store + } + + /** + * Removes the value for a key in the store. + * + * @param {Object} id The id to remove from the store. + * @memberOf Store + */ + lunr.Store.prototype.remove = function (id) { + if (!this.has(id)) return + + delete this.store[id] + this.length-- + } + + /** + * Returns a representation of the store ready for serialisation. + * + * @returns {Object} + * @memberOf Store + */ + lunr.Store.prototype.toJSON = function () { + return { + store: this.store, + length: this.length + } + } + + /*! + * lunr.stemmer + * Copyright (C) 2014 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + + /** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartaurs.org/~martin + * + * @module + * @param {String} str The string to stem + * @returns {String} + * @see lunr.Pipeline + */ + lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + var re_mgr0 = new RegExp(mgr0); + var re_mgr1 = new RegExp(mgr1); + var re_meq1 = new RegExp(meq1); + var re_s_v = new RegExp(s_v); + + var re_1a = /^(.+?)(ss|i)es$/; + var re2_1a = /^(.+?)([^s])s$/; + var re_1b = /^(.+?)eed$/; + var re2_1b = /^(.+?)(ed|ing)$/; + var re_1b_2 = /.$/; + var re2_1b_2 = /(at|bl|iz)$/; + var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); + var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var re_1c = /^(.+?[^aeiou])y$/; + var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + + var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + + var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + var re2_4 = /^(.+?)(s|t)(ion)$/; + + var re_5 = /^(.+?)e$/; + var re_5_1 = /ll$/; + var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var porterStemmer = function porterStemmer(w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = re_1a + re2 = re2_1a; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = re_1b; + re2 = re2_1b; + if (re.test(w)) { + var fp = re.exec(w); + re = re_mgr0; + if (re.test(fp[1])) { + re = re_1b_2; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = re_s_v; + if (re2.test(stem)) { + w = stem; + re2 = re2_1b_2; + re3 = re3_1b_2; + re4 = re4_1b_2; + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) + re = re_1c; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "i"; + } + + // Step 2 + re = re_2; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = re_3; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = re_4; + re2 = re2_4; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = re_mgr1; + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = re_5; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + re2 = re_meq1; + re3 = re3_5; + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = re_5_1; + re2 = re_mgr1; + if (re.test(w) && re2.test(w)) { + re = re_1b_2; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + }; + + return porterStemmer; + })(); + + lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') + /*! + * lunr.stopWordFilter + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @module + * @param {String} token The token to pass through the filter + * @returns {String} + * @see lunr.Pipeline + */ + lunr.stopWordFilter = function (token) { + if (lunr.stopWordFilter.stopWords.indexOf(token) === -1) return token + } + + lunr.stopWordFilter.stopWords = new lunr.SortedSet + lunr.stopWordFilter.stopWords.length = 119 + lunr.stopWordFilter.stopWords.elements = [ + "", + "a", + "able", + "about", + "across", + "after", + "all", + "almost", + "also", + "am", + "among", + "an", + "and", + "any", + "are", + "as", + "at", + "be", + "because", + "been", + "but", + "by", + "can", + "cannot", + "could", + "dear", + "did", + "do", + "does", + "either", + "else", + "ever", + "every", + "for", + "from", + "get", + "got", + "had", + "has", + "have", + "he", + "her", + "hers", + "him", + "his", + "how", + "however", + "i", + "if", + "in", + "into", + "is", + "it", + "its", + "just", + "least", + "let", + "like", + "likely", + "may", + "me", + "might", + "most", + "must", + "my", + "neither", + "no", + "nor", + "not", + "of", + "off", + "often", + "on", + "only", + "or", + "other", + "our", + "own", + "rather", + "said", + "say", + "says", + "she", + "should", + "since", + "so", + "some", + "than", + "that", + "the", + "their", + "them", + "then", + "there", + "these", + "they", + "this", + "tis", + "to", + "too", + "twas", + "us", + "wants", + "was", + "we", + "were", + "what", + "when", + "where", + "which", + "while", + "who", + "whom", + "why", + "will", + "with", + "would", + "yet", + "you", + "your" + ] + + lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') + /*! + * lunr.trimmer + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the begining and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @module + * @param {String} token The token to pass through the filter + * @returns {String} + * @see lunr.Pipeline + */ + lunr.trimmer = function (token) { + return token + .replace(/^\W+/, '') + .replace(/\W+$/, '') + } + + lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') + /*! + * lunr.stemmer + * Copyright (C) 2014 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + + /** + * lunr.TokenStore is used for efficient storing and lookup of the reverse + * index of token to document ref. + * + * @constructor + */ + lunr.TokenStore = function () { + this.root = { docs: {} } + this.length = 0 + } + + /** + * Loads a previously serialised token store + * + * @param {Object} serialisedData The serialised token store to load. + * @returns {lunr.TokenStore} + * @memberOf TokenStore + */ + lunr.TokenStore.load = function (serialisedData) { + var store = new this + + store.root = serialisedData.root + store.length = serialisedData.length + + return store + } + + /** + * Adds a new token doc pair to the store. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to store the doc under + * @param {Object} doc The doc to store against the token + * @param {Object} root An optional node at which to start looking for the + * correct place to enter the doc, by default the root of this lunr.TokenStore + * is used. + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.add = function (token, doc, root) { + var root = root || this.root, + key = token[0], + rest = token.slice(1) + + if (!(key in root)) root[key] = {docs: {}} + + if (rest.length === 0) { + root[key].docs[doc.ref] = doc + this.length += 1 + return + } else { + return this.add(rest, doc, root[key]) + } + } + + /** + * Checks whether this key is contained within this lunr.TokenStore. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to check for + * @param {Object} root An optional node at which to start + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.has = function (token) { + if (!token) return false + + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!node[token[i]]) return false + + node = node[token[i]] + } + + return true + } + + /** + * Retrieve a node from the token store for a given token. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the node for. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @see TokenStore.prototype.get + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.getNode = function (token) { + if (!token) return {} + + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!node[token[i]]) return {} + + node = node[token[i]] + } + + return node + } + + /** + * Retrieve the documents for a node for the given token. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the documents for. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.get = function (token, root) { + return this.getNode(token, root).docs || {} + } + + lunr.TokenStore.prototype.count = function (token, root) { + return Object.keys(this.get(token, root)).length + } + + /** + * Remove the document identified by ref from the token in the store. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the documents for. + * @param {String} ref The ref of the document to remove from this token. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.remove = function (token, ref) { + if (!token) return + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!(token[i] in node)) return + node = node[token[i]] + } + + delete node.docs[ref] + } + + /** + * Find all the possible suffixes of the passed token using tokens + * currently in the store. + * + * @param {String} token The token to expand. + * @returns {Array} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.expand = function (token, memo) { + var root = this.getNode(token), + docs = root.docs || {}, + memo = memo || [] + + if (Object.keys(docs).length) memo.push(token) + + Object.keys(root) + .forEach(function (key) { + if (key === 'docs') return + + memo.concat(this.expand(token + key, memo)) + }, this) + + return memo + } + + /** + * Returns a representation of the token store ready for serialisation. + * + * @returns {Object} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.toJSON = function () { + return { + root: this.root, + length: this.length + } + } + + + /** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/main/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like enviroments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})() +; +(function () { + 'use strict'; + + var searchInput, searchResults; + + var index = new lunr.Index(); + index.ref('id'); + index.field('title', { boost: 10 }); + index.pipeline.add(lunr.trimmer, lunr.stopWordFilter); + + $(populate); + $(bind); + $(initializeSlashHandler); + + function populate() { + $('h1, h2, h3').each(function() { + var title = $(this); + index.add({ + id: title.prop('id'), + title: title.text() + }); + }); + } + + function bind() { + searchInput = $('#input-search'); + searchResults = $('.search-results'); + searchInput.on('keyup', search); + } + + function initializeSlashHandler() { + $('body').on('keydown', function(event) { + if (event.keyCode == 191 && !searchInput.is(':focus')) { + event.preventDefault(); + searchResults.empty(); + searchInput.val('').focus(); + } + }); + } + + function search(event) { + searchResults.addClass('visible'); + + // ESC clears the field + if (event.keyCode === 27) this.value = ''; + + if (this.value) { + var results = index.search(this.value).filter(function(r) { + return r.score > 0.0001; + }); + + if (results.length) { + searchResults.empty(); + $.each(results, function (index, result) { + var elemId = '#' + result.ref; + searchResults.append("
  • " + $(elemId).text() + "
  • "); + }); + } else { + searchResults.html('
  • No results found for "' + this.value + '"
  • '); + } + } else { + searchResults.removeClass('visible'); + } + } +})(); +; +/*! jQuery UI - v1.11.3 - 2015-02-12 + * http://jqueryui.com + * Includes: widget.js + * Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + /*! + * jQuery UI Widget 1.11.3 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ + + + var widget_uuid = 0, + widget_slice = Array.prototype.slice; + + $.cleanData = (function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; (elem = elems[i]) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; + })( $.cleanData ); + + $.widget = function( name, base, prototype ) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split( "." )[ 0 ]; + + name = name.split( "." )[ 1 ]; + fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); + + return constructor; + }; + + $.widget.extend = function( target ) { + var input = widget_slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; + }; + + $.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string", + args = widget_slice.call( arguments, 1 ), + returnValue = this; + + if ( isMethodCall ) { + this.each(function() { + var methodValue, + instance = $.data( this, fullName ); + if ( options === "instance" ) { + returnValue = instance; + return false; + } + if ( !instance ) { + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); + } + methodValue = instance[ options ].apply( instance, args ); + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + }); + } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat(args) ); + } + + this.each(function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + }); + } + + return returnValue; + }; + }; + + $.Widget = function( /* options, element */ ) {}; + $.Widget._childConstructors = []; + + $.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
    ", + options: { + disabled: false, + + // callbacks + create: null + }, + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = widget_uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + }); + this.document = $( element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element ); + this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + } + + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this._create(); + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, + + destroy: function() { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .unbind( this.eventNamespace ) + .removeData( this.widgetFullName ) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData( $.camelCase( this.widgetFullName ) ); + this.widget() + .unbind( this.eventNamespace ) + .removeAttr( "aria-disabled" ) + .removeClass( + this.widgetFullName + "-disabled " + + "ui-state-disabled" ); + + // clean up events and states + this.bindings.unbind( this.eventNamespace ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + }, + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key, + parts, + curOption, + i; + + if ( arguments.length === 0 ) { + // don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + _setOption: function( key, value ) { + this.options[ key ] = value; + + if ( key === "disabled" ) { + this.widget() + .toggleClass( this.widgetFullName + "-disabled", !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } + } + + return this; + }, + + enable: function() { + return this._setOptions({ disabled: false }); + }, + disable: function() { + return this._setOptions({ disabled: true }); + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement, + instance = this; + + // no suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // no element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^([\w:-]*)\s*(.*)$/ ), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if ( selector ) { + delegateElement.delegate( selector, eventName, handlerProxy ); + } else { + element.bind( eventName, handlerProxy ); + } + }); + }, + + _off: function( element, eventName ) { + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; + element.unbind( eventName ).undelegate( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + $( event.currentTarget ).addClass( "ui-state-hover" ); + }, + mouseleave: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-hover" ); + } + }); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + $( event.currentTarget ).addClass( "ui-state-focus" ); + }, + focusout: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-focus" ); + } + }); + }, + + _trigger: function( type, event, data ) { + var prop, orig, + callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[0], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } + }; + + $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + if ( options.delay ) { + element.delay( options.delay ); + } + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue(function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + }); + } + }; + }); + + var widget = $.widget; + + + +})); +; +/* jquery Tocify - v1.8.0 - 2013-09-16 +* http://www.gregfranko.com/jquery.tocify.js/ +* Copyright (c) 2013 Greg Franko; Licensed MIT +* Modified lightly by Robert Lord to fix a bug I found, +* and also so it adds ids to headers +* also because I want height caching, since the +* height lookup for h1s and h2s was causing serious +* lag spikes below 30 fps */ + +(function(tocify) { + "use strict"; + tocify(window.jQuery, window, document); +} + +(function($, window, document, undefined) { + "use strict"; + + var tocFocusClassName = "toc-focus", + tocHoverClassName = "toc-hover", + headerClassName = "toc-header", + headerClass = "." + headerClassName, + subheaderClassName = "toc-subheader", + subheaderClass = "." + subheaderClassName, + itemClassName = "toc-link", + itemClass = "." + itemClassName; + + $.widget("toc.tocify", { + //Plugin version + version: "1.8.0", + + // These options will be used as defaults + options: { + // **context**: Accepts String: Any jQuery selector + // The container element that holds all of the elements used to generate the table of contents + context: "body", + + // **selectors**: Accepts an Array of Strings: Any jQuery selectors + // The element's used to generate the table of contents. The order is very important since it will determine the table of content's nesting structure + selectors: "h1, h2, h3", + + // **showEffectSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" + // The time duration of the show animation + showEffectSpeed: "medium", + + // **hideEffectSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" + // The time duration of the hide animation + hideEffectSpeed: "medium", + + // **scrollTo**: Accepts Number (pixels) + // The amount of space between the top of page and the selected table of contents item after the page has been scrolled + scrollTo: 0, + + // **highlightOffset**: Accepts a number + // The offset distance in pixels to trigger the next active table of contents item + highlightOffset: 1 + }, + + _create: function() { + var self = this; + + self.tocifyWrapper = $('.toc-wrapper'); + + self.cachedHeights = [], + self.cachedAnchors = []; + + self.focusClass = tocFocusClassName; + self.hoverClass = tocHoverClassName; + + self._setEventHandlers(); + + // Binding to the Window load event to make sure the correct scrollTop is calculated + $(window).load(function() { + self._setActiveElement(true); + }); + }, + + _setActiveElement: function(pageload) { + var self = this; + var hash = window.location.hash.substring(1); + var elem = self.element.find(".toc-link[href='#" + hash + "']"); + + self.element.find("." + self.focusClass).removeClass(self.focusClass); + + if (hash.length) { + elem.addClass(self.focusClass); + self._triggerShow(elem); + } else if (pageload) { + self.element.find(itemClass).first().addClass(self.focusClass); + } + + return self; + }, + + _setEventHandlers: function() { + var self = this; + + this.element.on("click.tocify", "a", function() { + self.element.find("." + self.focusClass).removeClass(self.focusClass); + $(this).addClass(self.focusClass); + }); + + $(window).on('resize', function() { + self._calculateHeights(); + }); + + $(window).on("scroll.tocify", function() { + // Once all animations on the page are complete, this callback function will be called + $("html, body").promise().done(function() { + // The zero timeout ensures the following code is run after the scroll events + setTimeout(function() { + if (self.cachedHeights.length == 0) { + self._calculateHeights(); + } + + var scrollTop = $(window).scrollTop(); + + var closestAnchorIdx = null; + self.cachedAnchors.each(function(idx) { + if (self.cachedHeights[idx] - scrollTop < 0) { + closestAnchorIdx = idx; + } else { + return false; + } + }); + + var anchorText = $(self.cachedAnchors[closestAnchorIdx]).attr("id"); + var elem = $('.toc-link[href="#' + anchorText + '"]'); + + if (elem.length && !elem.hasClass(self.focusClass)) { + self.element.find("." + self.focusClass).removeClass(self.focusClass); + elem.addClass(self.focusClass); + } + + if (window.location.hash !== "#" + anchorText && anchorText !== undefined) { + history.replaceState({}, "", "#" + anchorText); + } + + self._triggerShow(elem); + }, 0); + }); + }); + }, + + _calculateHeights: function() { + var self = this; + self.cachedAnchors = $(self.options.context).find(self.options.selectors); + self.cachedHeights = []; + + self.cachedAnchors.each(function(idx) { + self.cachedHeights[idx] = $(this).offset().top - self.options.highlightOffset; + }); + }, + + _show: function(elem) { + var self = this; + var parent = elem.parent() + + if (!elem.is(":visible")) { + // If the current element does not have any nested subheaders, is not a header, and its parent is not visible + if (!elem.find(subheaderClass).length && !parent.is(headerClass) && !parent.is(":visible")) { + // Sets the current element to all of the subheaders within the current header + elem = elem.parents(subheaderClass).add(elem); + } + + // If the current element does not have any nested subheaders and is not a header + else if (!elem.children(subheaderClass).length && !parent.is(headerClass)) { + // Sets the current element to the closest subheader + elem = elem.closest(subheaderClass); + } + + elem.slideDown(self.options.showEffectSpeed); + } + + // If the current subheader parent element is a header + if (parent.is(headerClass)) { + // Hides all non-active sub-headers + self.hide($(subheaderClass).not(elem)); + } + + // If the current subheader parent element is not a header + else { + // Hides all non-active sub-headers + self.hide($(subheaderClass).not(elem.closest(headerClass).find(subheaderClass).not(elem.siblings()))); + } + + return self; + }, + + hide: function(elem) { + var self = this; + + elem.slideUp(self.options.hideEffectSpeed); + + return self; + }, + + _triggerShow: function(linkElem) { + var self = this; + var itemElem = linkElem.parent(); + + if (itemElem.parent().is(headerClass) || itemElem.next().is(subheaderClass)) { + // Shows the next sub-header element + self._show(itemElem.next(subheaderClass)); + } else if (itemElem.parent().is(subheaderClass)) { + // Shows the parent sub-header element + self._show(itemElem.parent()); + } + + return self; + }, + + setOption: function() { + $.Widget.prototype._setOption.apply(this, arguments); + } + }); +})); +; +/*! + * imagesLoaded PACKAGED v3.1.8 + * JavaScript is all like "You images are done yet or what?" + * MIT License + */ + +(function(){function e(){}function t(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function n(e){return function(){return this[e].apply(this,arguments)}}var i=e.prototype,r=this,o=r.EventEmitter;i.getListeners=function(e){var t,n,i=this._getEvents();if("object"==typeof e){t={};for(n in i)i.hasOwnProperty(n)&&e.test(n)&&(t[n]=i[n])}else t=i[e]||(i[e]=[]);return t},i.flattenListeners=function(e){var t,n=[];for(t=0;e.length>t;t+=1)n.push(e[t].listener);return n},i.getListenersAsObject=function(e){var t,n=this.getListeners(e);return n instanceof Array&&(t={},t[e]=n),t||n},i.addListener=function(e,n){var i,r=this.getListenersAsObject(e),o="object"==typeof n;for(i in r)r.hasOwnProperty(i)&&-1===t(r[i],n)&&r[i].push(o?n:{listener:n,once:!1});return this},i.on=n("addListener"),i.addOnceListener=function(e,t){return this.addListener(e,{listener:t,once:!0})},i.once=n("addOnceListener"),i.defineEvent=function(e){return this.getListeners(e),this},i.defineEvents=function(e){for(var t=0;e.length>t;t+=1)this.defineEvent(e[t]);return this},i.removeListener=function(e,n){var i,r,o=this.getListenersAsObject(e);for(r in o)o.hasOwnProperty(r)&&(i=t(o[r],n),-1!==i&&o[r].splice(i,1));return this},i.off=n("removeListener"),i.addListeners=function(e,t){return this.manipulateListeners(!1,e,t)},i.removeListeners=function(e,t){return this.manipulateListeners(!0,e,t)},i.manipulateListeners=function(e,t,n){var i,r,o=e?this.removeListener:this.addListener,s=e?this.removeListeners:this.addListeners;if("object"!=typeof t||t instanceof RegExp)for(i=n.length;i--;)o.call(this,t,n[i]);else for(i in t)t.hasOwnProperty(i)&&(r=t[i])&&("function"==typeof r?o.call(this,i,r):s.call(this,i,r));return this},i.removeEvent=function(e){var t,n=typeof e,i=this._getEvents();if("string"===n)delete i[e];else if("object"===n)for(t in i)i.hasOwnProperty(t)&&e.test(t)&&delete i[t];else delete this._events;return this},i.removeAllListeners=n("removeEvent"),i.emitEvent=function(e,t){var n,i,r,o,s=this.getListenersAsObject(e);for(r in s)if(s.hasOwnProperty(r))for(i=s[r].length;i--;)n=s[r][i],n.once===!0&&this.removeListener(e,n.listener),o=n.listener.apply(this,t||[]),o===this._getOnceReturnValue()&&this.removeListener(e,n.listener);return this},i.trigger=n("emitEvent"),i.emit=function(e){var t=Array.prototype.slice.call(arguments,1);return this.emitEvent(e,t)},i.setOnceReturnValue=function(e){return this._onceReturnValue=e,this},i._getOnceReturnValue=function(){return this.hasOwnProperty("_onceReturnValue")?this._onceReturnValue:!0},i._getEvents=function(){return this._events||(this._events={})},e.noConflict=function(){return r.EventEmitter=o,e},"function"==typeof define&&define.amd?define("eventEmitter/EventEmitter",[],function(){return e}):"object"==typeof module&&module.exports?module.exports=e:this.EventEmitter=e}).call(this),function(e){function t(t){var n=e.event;return n.target=n.target||n.srcElement||t,n}var n=document.documentElement,i=function(){};n.addEventListener?i=function(e,t,n){e.addEventListener(t,n,!1)}:n.attachEvent&&(i=function(e,n,i){e[n+i]=i.handleEvent?function(){var n=t(e);i.handleEvent.call(i,n)}:function(){var n=t(e);i.call(e,n)},e.attachEvent("on"+n,e[n+i])});var r=function(){};n.removeEventListener?r=function(e,t,n){e.removeEventListener(t,n,!1)}:n.detachEvent&&(r=function(e,t,n){e.detachEvent("on"+t,e[t+n]);try{delete e[t+n]}catch(i){e[t+n]=void 0}});var o={bind:i,unbind:r};"function"==typeof define&&define.amd?define("eventie/eventie",o):e.eventie=o}(this),function(e,t){"function"==typeof define&&define.amd?define(["eventEmitter/EventEmitter","eventie/eventie"],function(n,i){return t(e,n,i)}):"object"==typeof exports?module.exports=t(e,require("wolfy87-eventemitter"),require("eventie")):e.imagesLoaded=t(e,e.EventEmitter,e.eventie)}(window,function(e,t,n){function i(e,t){for(var n in t)e[n]=t[n];return e}function r(e){return"[object Array]"===d.call(e)}function o(e){var t=[];if(r(e))t=e;else if("number"==typeof e.length)for(var n=0,i=e.length;i>n;n++)t.push(e[n]);else t.push(e);return t}function s(e,t,n){if(!(this instanceof s))return new s(e,t);"string"==typeof e&&(e=document.querySelectorAll(e)),this.elements=o(e),this.options=i({},this.options),"function"==typeof t?n=t:i(this.options,t),n&&this.on("always",n),this.getImages(),a&&(this.jqDeferred=new a.Deferred);var r=this;setTimeout(function(){r.check()})}function f(e){this.img=e}function c(e){this.src=e,v[e]=this}var a=e.jQuery,u=e.console,h=u!==void 0,d=Object.prototype.toString;s.prototype=new t,s.prototype.options={},s.prototype.getImages=function(){this.images=[];for(var e=0,t=this.elements.length;t>e;e++){var n=this.elements[e];"IMG"===n.nodeName&&this.addImage(n);var i=n.nodeType;if(i&&(1===i||9===i||11===i))for(var r=n.querySelectorAll("img"),o=0,s=r.length;s>o;o++){var f=r[o];this.addImage(f)}}},s.prototype.addImage=function(e){var t=new f(e);this.images.push(t)},s.prototype.check=function(){function e(e,r){return t.options.debug&&h&&u.log("confirm",e,r),t.progress(e),n++,n===i&&t.complete(),!0}var t=this,n=0,i=this.images.length;if(this.hasAnyBroken=!1,!i)return this.complete(),void 0;for(var r=0;i>r;r++){var o=this.images[r];o.on("confirm",e),o.check()}},s.prototype.progress=function(e){this.hasAnyBroken=this.hasAnyBroken||!e.isLoaded;var t=this;setTimeout(function(){t.emit("progress",t,e),t.jqDeferred&&t.jqDeferred.notify&&t.jqDeferred.notify(t,e)})},s.prototype.complete=function(){var e=this.hasAnyBroken?"fail":"done";this.isComplete=!0;var t=this;setTimeout(function(){if(t.emit(e,t),t.emit("always",t),t.jqDeferred){var n=t.hasAnyBroken?"reject":"resolve";t.jqDeferred[n](t)}})},a&&(a.fn.imagesLoaded=function(e,t){var n=new s(this,e,t);return n.jqDeferred.promise(a(this))}),f.prototype=new t,f.prototype.check=function(){var e=v[this.img.src]||new c(this.img.src);if(e.isConfirmed)return this.confirm(e.isLoaded,"cached was confirmed"),void 0;if(this.img.complete&&void 0!==this.img.naturalWidth)return this.confirm(0!==this.img.naturalWidth,"naturalWidth"),void 0;var t=this;e.on("confirm",function(e,n){return t.confirm(e.isLoaded,n),!0}),e.check()},f.prototype.confirm=function(e,t){this.isLoaded=e,this.emit("confirm",this,t)};var v={};return c.prototype=new t,c.prototype.check=function(){if(!this.isChecked){var e=new Image;n.bind(e,"load",this),n.bind(e,"error",this),e.src=this.src,this.isChecked=!0}},c.prototype.handleEvent=function(e){var t="on"+e.type;this[t]&&this[t](e)},c.prototype.onload=function(e){this.confirm(!0,"onload"),this.unbindProxyEvents(e)},c.prototype.onerror=function(e){this.confirm(!1,"onerror"),this.unbindProxyEvents(e)},c.prototype.confirm=function(e,t){this.isConfirmed=!0,this.isLoaded=e,this.emit("confirm",this,t)},c.prototype.unbindProxyEvents=function(e){n.unbind(e.target,"load",this),n.unbind(e.target,"error",this)},s});; +(function (global) { + 'use strict'; + + var closeToc = function() { + $(".toc-wrapper").removeClass('open'); + $("#nav-button").removeClass('open'); + }; + + var makeToc = function() { + global.toc = $("#toc").tocify({ + selectors: 'h1, h2, h3', + showEffectSpeed: 0, + hideEffectSpeed: 180, + }).data('toc-tocify'); + + $("#nav-button").click(function() { + $(".toc-wrapper").toggleClass('open'); + $("#nav-button").toggleClass('open'); + return false; + }); + + $(".page-wrapper").click(closeToc); + $(".tocify-item").click(closeToc); + }; + + // Hack to make already open sections to start opened, + // instead of displaying an ugly animation + function animate() { + setTimeout(function() { + toc.setOption('showEffectSpeed', 180); + }, 50); + } + + $(function() { + makeToc(); + animate(); + $('.content').imagesLoaded( function() { + toc._calculateHeights(); + }); + }); +})(window); + +; +(function() { + 'use strict'; + + $(setupVersionsDropdown); + + function setupVersionsDropdown() { + $.get( + "/versions.json", + function(data) { + var versions = data.versions; + + for (var i = 0; i < versions.length; i++) { + var version = versions[i]; + if (version == "release-candidate") continue; + + var li = '
  • ' + version + '
  • '; + $('#version-list').append(li); + } + } + ); + } +})(); + From 0575b8ae88f75eae1a17a8eac8022466348a506d Mon Sep 17 00:00:00 2001 From: johha Date: Mon, 23 Feb 2026 12:03:51 +0100 Subject: [PATCH 2/5] Implement external JavaScript bundler for docs/v3 Replace Sprockets-based JavaScript concatenation with a custom Node.js build script that handles bundling during development and build time. Changes: - Add webrick gem for Rack 3 / Middleman 4.6 compatibility - Create build-js.mjs script to concatenate JavaScript files in dependency order - Update package.json to add build:js script and integrate it into the start command - Regenerate all.js with proper file markers and comments - Update config.rb comment to reference new build system Benefits: - Cleaner separation of concerns (Ruby for templates, Node for JS) - No conflict between Sprockets and Middleman's Sass renderer - Maintainable individual source files in lib/ and app/ directories - Automatic bundling on npm start and can be run manually with npm run build:js This completes the Option 4 implementation for long-term maintainability. --- docs/v3/Gemfile | 1 + docs/v3/Gemfile.lock | 1 + docs/v3/build-js.mjs | 52 +++++++++++++++++++++++++++++++ docs/v3/config.rb | 2 +- docs/v3/package.json | 3 +- docs/v3/source/javascripts/all.js | 44 +++++++++++++------------- 6 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 docs/v3/build-js.mjs diff --git a/docs/v3/Gemfile b/docs/v3/Gemfile index 3dd9862071..4ec9fc642d 100644 --- a/docs/v3/Gemfile +++ b/docs/v3/Gemfile @@ -14,3 +14,4 @@ gem 'nokogiri', '~> 1.19.1' gem 'rake', '>= 12.3.3' gem 'redcarpet', '~> 3.6.1' gem 'sass' +gem 'webrick' diff --git a/docs/v3/Gemfile.lock b/docs/v3/Gemfile.lock index 3bc3e7b34a..af59e9524d 100644 --- a/docs/v3/Gemfile.lock +++ b/docs/v3/Gemfile.lock @@ -180,6 +180,7 @@ DEPENDENCIES rake (>= 12.3.3) redcarpet (~> 3.6.1) sass + webrick BUNDLED WITH 2.6.9 diff --git a/docs/v3/build-js.mjs b/docs/v3/build-js.mjs new file mode 100644 index 0000000000..7742e0718f --- /dev/null +++ b/docs/v3/build-js.mjs @@ -0,0 +1,52 @@ +// JavaScript bundler for docs/v3 +// This concatenates all JavaScript files in the correct dependency order +// Run this whenever you modify individual JS files in lib/ or app/ + +import { readFileSync, writeFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Define files in dependency order (same as original Sprockets manifest) +const files = [ + 'lib/_energize.js', + 'lib/_dropdown.js', + 'lib/_lunr.js', + 'app/_search.js', + 'lib/_jquery_ui.js', + 'lib/_jquery.tocify.js', + 'lib/_imagesloaded.min.js', + 'app/_toc.js', + 'app/_version_dropdown.js' +]; + +const sourceDir = join(__dirname, 'source/javascripts'); +const outputFile = join(sourceDir, 'all.js'); + +// Create bundle header +let bundleContent = `// Auto-generated JavaScript bundle +// Generated by: npm run build:js +// Source files are in lib/ and app/ directories +// DO NOT edit this file directly - edit the source files and regenerate + +`; + +// Concatenate files with semicolons between them +for (const file of files) { + const filePath = join(sourceDir, file); + const content = readFileSync(filePath, 'utf-8'); + // Skip Sprockets directives from individual files + const cleanContent = content + .split('\n') + .filter(line => !line.trim().startsWith('//= require')) + .join('\n'); + + bundleContent += `// From: ${file}\n`; + bundleContent += cleanContent + ';\n\n'; +} + +// Write the bundle to source directory (Middleman will copy it to build) +writeFileSync(outputFile, bundleContent); +console.log(`✓ JavaScript bundle created: ${outputFile}`); diff --git a/docs/v3/config.rb b/docs/v3/config.rb index 7f054a2464..f3a6f417c9 100644 --- a/docs/v3/config.rb +++ b/docs/v3/config.rb @@ -21,7 +21,7 @@ activate :syntax # Sprockets removed - caused conflicts with Sass rendering -# We'll handle JS concatenation differently +# JavaScript bundling is now handled by esbuild (see build-js.mjs and package.json scripts) # activate :sprockets activate :autoprefixer do |config| diff --git a/docs/v3/package.json b/docs/v3/package.json index 43a12f4543..7cfc10b7e9 100644 --- a/docs/v3/package.json +++ b/docs/v3/package.json @@ -4,7 +4,8 @@ "description": "Cloud Foundry API Docs ========", "main": "index.js", "scripts": { - "start": "./node_modules/.bin/gulp", + "build:js": "node build-js.mjs", + "start": "npm run build:js && ./node_modules/.bin/gulp", "test": "echo \"Error: no test specified\" && exit 1", "checkdocs": "./node_modules/.bin/gulp checkdocs" }, diff --git a/docs/v3/source/javascripts/all.js b/docs/v3/source/javascripts/all.js index 688d92ee75..864473fe96 100644 --- a/docs/v3/source/javascripts/all.js +++ b/docs/v3/source/javascripts/all.js @@ -1,25 +1,9 @@ -// Concatenated JavaScript file for docs/v3 -// This file was created because middleman-sprockets conflicts with Middleman 4.6+ Sass rendering -// -// To regenerate this file, run from docs/v3 directory: -// cat source/javascripts/lib/_energize.js > source/javascripts/all.js && \ -// echo ";" >> source/javascripts/all.js && \ -// cat source/javascripts/lib/_dropdown.js >> source/javascripts/all.js && \ -// echo ";" >> source/javascripts/all.js && \ -// cat source/javascripts/lib/_lunr.js >> source/javascripts/all.js && \ -// echo ";" >> source/javascripts/all.js && \ -// tail -n +2 source/javascripts/app/_search.js >> source/javascripts/all.js && \ -// echo ";" >> source/javascripts/all.js && \ -// cat source/javascripts/lib/_jquery_ui.js >> source/javascripts/all.js && \ -// echo ";" >> source/javascripts/all.js && \ -// cat source/javascripts/lib/_jquery.tocify.js >> source/javascripts/all.js && \ -// echo ";" >> source/javascripts/all.js && \ -// cat source/javascripts/lib/_imagesloaded.min.js >> source/javascripts/all.js && \ -// echo ";" >> source/javascripts/all.js && \ -// tail -n +4 source/javascripts/app/_toc.js >> source/javascripts/all.js && \ -// echo ";" >> source/javascripts/all.js && \ -// cat source/javascripts/app/_version_dropdown.js >> source/javascripts/all.js +// Auto-generated JavaScript bundle +// Generated by: npm run build:js +// Source files are in lib/ and app/ directories +// DO NOT edit this file directly - edit the source files and regenerate +// From: lib/_energize.js /** * energize.js v0.1.0 * @@ -189,6 +173,8 @@ document.addEventListener('click', click, true); // TODO: why does this use capture? })();; + +// From: lib/_dropdown.js /* ======================================================================== * Bootstrap: dropdown.js v3.3.6 * http://getbootstrap.com/javascript/#dropdowns @@ -355,6 +341,8 @@ }(jQuery); ; + +// From: lib/_lunr.js /** * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 0.5.7 * Copyright (C) 2014 Oliver Nightingale @@ -2266,6 +2254,8 @@ })) })() ; + +// From: app/_search.js (function () { 'use strict'; @@ -2332,6 +2322,8 @@ } })(); ; + +// From: lib/_jquery_ui.js /*! jQuery UI - v1.11.3 - 2015-02-12 * http://jqueryui.com * Includes: widget.js @@ -2899,6 +2891,8 @@ })); ; + +// From: lib/_jquery.tocify.js /* jquery Tocify - v1.8.0 - 2013-09-16 * http://www.gregfranko.com/jquery.tocify.js/ * Copyright (c) 2013 Greg Franko; Licensed MIT @@ -3116,6 +3110,8 @@ }); })); ; + +// From: lib/_imagesloaded.min.js /*! * imagesLoaded PACKAGED v3.1.8 * JavaScript is all like "You images are done yet or what?" @@ -3123,6 +3119,8 @@ */ (function(){function e(){}function t(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function n(e){return function(){return this[e].apply(this,arguments)}}var i=e.prototype,r=this,o=r.EventEmitter;i.getListeners=function(e){var t,n,i=this._getEvents();if("object"==typeof e){t={};for(n in i)i.hasOwnProperty(n)&&e.test(n)&&(t[n]=i[n])}else t=i[e]||(i[e]=[]);return t},i.flattenListeners=function(e){var t,n=[];for(t=0;e.length>t;t+=1)n.push(e[t].listener);return n},i.getListenersAsObject=function(e){var t,n=this.getListeners(e);return n instanceof Array&&(t={},t[e]=n),t||n},i.addListener=function(e,n){var i,r=this.getListenersAsObject(e),o="object"==typeof n;for(i in r)r.hasOwnProperty(i)&&-1===t(r[i],n)&&r[i].push(o?n:{listener:n,once:!1});return this},i.on=n("addListener"),i.addOnceListener=function(e,t){return this.addListener(e,{listener:t,once:!0})},i.once=n("addOnceListener"),i.defineEvent=function(e){return this.getListeners(e),this},i.defineEvents=function(e){for(var t=0;e.length>t;t+=1)this.defineEvent(e[t]);return this},i.removeListener=function(e,n){var i,r,o=this.getListenersAsObject(e);for(r in o)o.hasOwnProperty(r)&&(i=t(o[r],n),-1!==i&&o[r].splice(i,1));return this},i.off=n("removeListener"),i.addListeners=function(e,t){return this.manipulateListeners(!1,e,t)},i.removeListeners=function(e,t){return this.manipulateListeners(!0,e,t)},i.manipulateListeners=function(e,t,n){var i,r,o=e?this.removeListener:this.addListener,s=e?this.removeListeners:this.addListeners;if("object"!=typeof t||t instanceof RegExp)for(i=n.length;i--;)o.call(this,t,n[i]);else for(i in t)t.hasOwnProperty(i)&&(r=t[i])&&("function"==typeof r?o.call(this,i,r):s.call(this,i,r));return this},i.removeEvent=function(e){var t,n=typeof e,i=this._getEvents();if("string"===n)delete i[e];else if("object"===n)for(t in i)i.hasOwnProperty(t)&&e.test(t)&&delete i[t];else delete this._events;return this},i.removeAllListeners=n("removeEvent"),i.emitEvent=function(e,t){var n,i,r,o,s=this.getListenersAsObject(e);for(r in s)if(s.hasOwnProperty(r))for(i=s[r].length;i--;)n=s[r][i],n.once===!0&&this.removeListener(e,n.listener),o=n.listener.apply(this,t||[]),o===this._getOnceReturnValue()&&this.removeListener(e,n.listener);return this},i.trigger=n("emitEvent"),i.emit=function(e){var t=Array.prototype.slice.call(arguments,1);return this.emitEvent(e,t)},i.setOnceReturnValue=function(e){return this._onceReturnValue=e,this},i._getOnceReturnValue=function(){return this.hasOwnProperty("_onceReturnValue")?this._onceReturnValue:!0},i._getEvents=function(){return this._events||(this._events={})},e.noConflict=function(){return r.EventEmitter=o,e},"function"==typeof define&&define.amd?define("eventEmitter/EventEmitter",[],function(){return e}):"object"==typeof module&&module.exports?module.exports=e:this.EventEmitter=e}).call(this),function(e){function t(t){var n=e.event;return n.target=n.target||n.srcElement||t,n}var n=document.documentElement,i=function(){};n.addEventListener?i=function(e,t,n){e.addEventListener(t,n,!1)}:n.attachEvent&&(i=function(e,n,i){e[n+i]=i.handleEvent?function(){var n=t(e);i.handleEvent.call(i,n)}:function(){var n=t(e);i.call(e,n)},e.attachEvent("on"+n,e[n+i])});var r=function(){};n.removeEventListener?r=function(e,t,n){e.removeEventListener(t,n,!1)}:n.detachEvent&&(r=function(e,t,n){e.detachEvent("on"+t,e[t+n]);try{delete e[t+n]}catch(i){e[t+n]=void 0}});var o={bind:i,unbind:r};"function"==typeof define&&define.amd?define("eventie/eventie",o):e.eventie=o}(this),function(e,t){"function"==typeof define&&define.amd?define(["eventEmitter/EventEmitter","eventie/eventie"],function(n,i){return t(e,n,i)}):"object"==typeof exports?module.exports=t(e,require("wolfy87-eventemitter"),require("eventie")):e.imagesLoaded=t(e,e.EventEmitter,e.eventie)}(window,function(e,t,n){function i(e,t){for(var n in t)e[n]=t[n];return e}function r(e){return"[object Array]"===d.call(e)}function o(e){var t=[];if(r(e))t=e;else if("number"==typeof e.length)for(var n=0,i=e.length;i>n;n++)t.push(e[n]);else t.push(e);return t}function s(e,t,n){if(!(this instanceof s))return new s(e,t);"string"==typeof e&&(e=document.querySelectorAll(e)),this.elements=o(e),this.options=i({},this.options),"function"==typeof t?n=t:i(this.options,t),n&&this.on("always",n),this.getImages(),a&&(this.jqDeferred=new a.Deferred);var r=this;setTimeout(function(){r.check()})}function f(e){this.img=e}function c(e){this.src=e,v[e]=this}var a=e.jQuery,u=e.console,h=u!==void 0,d=Object.prototype.toString;s.prototype=new t,s.prototype.options={},s.prototype.getImages=function(){this.images=[];for(var e=0,t=this.elements.length;t>e;e++){var n=this.elements[e];"IMG"===n.nodeName&&this.addImage(n);var i=n.nodeType;if(i&&(1===i||9===i||11===i))for(var r=n.querySelectorAll("img"),o=0,s=r.length;s>o;o++){var f=r[o];this.addImage(f)}}},s.prototype.addImage=function(e){var t=new f(e);this.images.push(t)},s.prototype.check=function(){function e(e,r){return t.options.debug&&h&&u.log("confirm",e,r),t.progress(e),n++,n===i&&t.complete(),!0}var t=this,n=0,i=this.images.length;if(this.hasAnyBroken=!1,!i)return this.complete(),void 0;for(var r=0;i>r;r++){var o=this.images[r];o.on("confirm",e),o.check()}},s.prototype.progress=function(e){this.hasAnyBroken=this.hasAnyBroken||!e.isLoaded;var t=this;setTimeout(function(){t.emit("progress",t,e),t.jqDeferred&&t.jqDeferred.notify&&t.jqDeferred.notify(t,e)})},s.prototype.complete=function(){var e=this.hasAnyBroken?"fail":"done";this.isComplete=!0;var t=this;setTimeout(function(){if(t.emit(e,t),t.emit("always",t),t.jqDeferred){var n=t.hasAnyBroken?"reject":"resolve";t.jqDeferred[n](t)}})},a&&(a.fn.imagesLoaded=function(e,t){var n=new s(this,e,t);return n.jqDeferred.promise(a(this))}),f.prototype=new t,f.prototype.check=function(){var e=v[this.img.src]||new c(this.img.src);if(e.isConfirmed)return this.confirm(e.isLoaded,"cached was confirmed"),void 0;if(this.img.complete&&void 0!==this.img.naturalWidth)return this.confirm(0!==this.img.naturalWidth,"naturalWidth"),void 0;var t=this;e.on("confirm",function(e,n){return t.confirm(e.isLoaded,n),!0}),e.check()},f.prototype.confirm=function(e,t){this.isLoaded=e,this.emit("confirm",this,t)};var v={};return c.prototype=new t,c.prototype.check=function(){if(!this.isChecked){var e=new Image;n.bind(e,"load",this),n.bind(e,"error",this),e.src=this.src,this.isChecked=!0}},c.prototype.handleEvent=function(e){var t="on"+e.type;this[t]&&this[t](e)},c.prototype.onload=function(e){this.confirm(!0,"onload"),this.unbindProxyEvents(e)},c.prototype.onerror=function(e){this.confirm(!1,"onerror"),this.unbindProxyEvents(e)},c.prototype.confirm=function(e,t){this.isConfirmed=!0,this.isLoaded=e,this.emit("confirm",this,t)},c.prototype.unbindProxyEvents=function(e){n.unbind(e.target,"load",this),n.unbind(e.target,"error",this)},s});; + +// From: app/_toc.js (function (global) { 'use strict'; @@ -3166,6 +3164,8 @@ })(window); ; + +// From: app/_version_dropdown.js (function() { 'use strict'; @@ -3189,3 +3189,5 @@ } })(); +; + From 7df30978e17b8bc80a05a7c30cc39cd915c940a9 Mon Sep 17 00:00:00 2001 From: johha Date: Mon, 23 Feb 2026 12:06:43 +0100 Subject: [PATCH 3/5] Document JavaScript build system in README Add section explaining the new JavaScript build workflow: - How the build script concatenates individual source files - When developers need to run npm run build:js - Warning not to edit all.js directly since it's auto-generated - Where to modify the build configuration if needed This helps future contributors understand the build system. --- docs/v3/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/v3/README.md b/docs/v3/README.md index cbb63852d3..30b7c20f80 100644 --- a/docs/v3/README.md +++ b/docs/v3/README.md @@ -13,6 +13,17 @@ Getting Started - Start the test server: `npm start` - You can now see the docs at . +Working with JavaScript +----------------------- + +JavaScript files are maintained as separate source files in `source/javascripts/lib/` and `source/javascripts/app/` directories, then concatenated into `source/javascripts/all.js` by a build script. + +- **During development**: `npm start` automatically rebuilds the JavaScript bundle before starting the server +- **Manual rebuild**: If you modify any JavaScript files in `lib/` or `app/`, run `npm run build:js` to regenerate `all.js` +- **DO NOT edit** `source/javascripts/all.js` directly - it's auto-generated and your changes will be overwritten + +The build script (`build-js.mjs`) concatenates files in the correct dependency order. If you need to add or reorder JavaScript files, edit the `files` array in `build-js.mjs`. + Making New Files ---------------- From 9480e3ee781643ebca4e27ec808df4f8b7413128 Mon Sep 17 00:00:00 2001 From: johha Date: Mon, 23 Feb 2026 12:31:13 +0100 Subject: [PATCH 4/5] Add dark mode with theme toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement a complete dark mode solution with automatic system preference detection and manual toggle control. Features: - Automatic dark mode detection via prefers-color-scheme media query - Manual theme toggle button (cycles: Auto → Light → Dark → Auto) - Theme preference persisted in localStorage - Minimal icon-only toggle button (sun/moon) below logo CSS changes: - Convert colors to CSS custom properties for theme switching - Add dark color palette with proper contrast ratios - Update all UI elements: sidebar, navigation, tables, borders, version dropdown, search, and inline code - Logo inverts colors in dark mode for visibility - Code blocks (JSON examples) maintain their Monokai syntax highlighting in both themes JavaScript: - New theme_toggle.js module handles theme switching logic - Detects system preference changes automatically - Updates toggle icon based on current theme - Integrated into build-js.mjs bundle The implementation respects user system preferences by default while allowing manual override for flexibility. --- docs/v3/build-js.mjs | 3 +- docs/v3/source/javascripts/all.js | 118 ++++++++++++++++++ .../source/javascripts/app/_theme_toggle.js | 115 +++++++++++++++++ docs/v3/source/layouts/layout.erb | 4 + docs/v3/source/stylesheets/_theme-toggle.scss | 26 ++++ docs/v3/source/stylesheets/_variables.scss | 72 +++++++++++ .../source/stylesheets/_version-dropdown.scss | 20 +-- docs/v3/source/stylesheets/screen.css.scss | 69 ++++++---- 8 files changed, 394 insertions(+), 33 deletions(-) create mode 100644 docs/v3/source/javascripts/app/_theme_toggle.js create mode 100644 docs/v3/source/stylesheets/_theme-toggle.scss diff --git a/docs/v3/build-js.mjs b/docs/v3/build-js.mjs index 7742e0718f..14ede29ea9 100644 --- a/docs/v3/build-js.mjs +++ b/docs/v3/build-js.mjs @@ -19,7 +19,8 @@ const files = [ 'lib/_jquery.tocify.js', 'lib/_imagesloaded.min.js', 'app/_toc.js', - 'app/_version_dropdown.js' + 'app/_version_dropdown.js', + 'app/_theme_toggle.js' ]; const sourceDir = join(__dirname, 'source/javascripts'); diff --git a/docs/v3/source/javascripts/all.js b/docs/v3/source/javascripts/all.js index 864473fe96..7a3fb2b05a 100644 --- a/docs/v3/source/javascripts/all.js +++ b/docs/v3/source/javascripts/all.js @@ -3191,3 +3191,121 @@ ; +// From: app/_theme_toggle.js +(function() { + 'use strict'; + + // Theme management + var STORAGE_KEY = 'docs-theme'; + var THEME_AUTO = 'auto'; + var THEME_LIGHT = 'light'; + var THEME_DARK = 'dark'; + + function getSystemTheme() { + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return THEME_DARK; + } + return THEME_LIGHT; + } + + function getStoredTheme() { + try { + return localStorage.getItem(STORAGE_KEY) || THEME_AUTO; + } catch (e) { + return THEME_AUTO; + } + } + + function setStoredTheme(theme) { + try { + localStorage.setItem(STORAGE_KEY, theme); + } catch (e) { + // localStorage might be disabled + } + } + + function applyTheme(theme) { + var effectiveTheme = theme; + + if (theme === THEME_AUTO) { + effectiveTheme = getSystemTheme(); + document.documentElement.removeAttribute('data-theme'); + } else { + document.documentElement.setAttribute('data-theme', theme); + } + + updateToggleButton(theme); + } + + function updateToggleButton(currentTheme) { + var button = document.getElementById('theme-toggle'); + if (!button) return; + + var icon = button.querySelector('.theme-icon'); + if (!icon) return; + + // Update icon and title based on current theme + if (currentTheme === THEME_AUTO) { + var systemTheme = getSystemTheme(); + icon.textContent = systemTheme === THEME_DARK ? '☀' : '☾'; + button.title = 'Theme: Auto (' + (systemTheme === 'dark' ? 'Dark' : 'Light') + ')'; + } else if (currentTheme === THEME_LIGHT) { + icon.textContent = '☾'; + button.title = 'Theme: Light (click for Dark)'; + } else { + icon.textContent = '☀'; + button.title = 'Theme: Dark (click for Auto)'; + } + } + + function cycleTheme() { + var current = getStoredTheme(); + var next; + + // Cycle: auto -> light -> dark -> auto + if (current === THEME_AUTO) { + next = THEME_LIGHT; + } else if (current === THEME_LIGHT) { + next = THEME_DARK; + } else { + next = THEME_AUTO; + } + + setStoredTheme(next); + applyTheme(next); + } + + // Initialize theme on page load + function initTheme() { + var stored = getStoredTheme(); + applyTheme(stored); + + // Listen for system theme changes + if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: dark)').addListener(function() { + var currentStored = getStoredTheme(); + if (currentStored === THEME_AUTO) { + applyTheme(THEME_AUTO); + } + }); + } + + // Set up toggle button click handler + var button = document.getElementById('theme-toggle'); + if (button) { + button.addEventListener('click', function(e) { + e.preventDefault(); + cycleTheme(); + }); + } + } + + // Run on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initTheme); + } else { + initTheme(); + } +})(); +; + diff --git a/docs/v3/source/javascripts/app/_theme_toggle.js b/docs/v3/source/javascripts/app/_theme_toggle.js new file mode 100644 index 0000000000..71d665854e --- /dev/null +++ b/docs/v3/source/javascripts/app/_theme_toggle.js @@ -0,0 +1,115 @@ +(function() { + 'use strict'; + + // Theme management + var STORAGE_KEY = 'docs-theme'; + var THEME_AUTO = 'auto'; + var THEME_LIGHT = 'light'; + var THEME_DARK = 'dark'; + + function getSystemTheme() { + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return THEME_DARK; + } + return THEME_LIGHT; + } + + function getStoredTheme() { + try { + return localStorage.getItem(STORAGE_KEY) || THEME_AUTO; + } catch (e) { + return THEME_AUTO; + } + } + + function setStoredTheme(theme) { + try { + localStorage.setItem(STORAGE_KEY, theme); + } catch (e) { + // localStorage might be disabled + } + } + + function applyTheme(theme) { + var effectiveTheme = theme; + + if (theme === THEME_AUTO) { + effectiveTheme = getSystemTheme(); + document.documentElement.removeAttribute('data-theme'); + } else { + document.documentElement.setAttribute('data-theme', theme); + } + + updateToggleButton(theme); + } + + function updateToggleButton(currentTheme) { + var button = document.getElementById('theme-toggle'); + if (!button) return; + + var icon = button.querySelector('.theme-icon'); + if (!icon) return; + + // Update icon and title based on current theme + if (currentTheme === THEME_AUTO) { + var systemTheme = getSystemTheme(); + icon.textContent = systemTheme === THEME_DARK ? '☀' : '☾'; + button.title = 'Theme: Auto (' + (systemTheme === 'dark' ? 'Dark' : 'Light') + ')'; + } else if (currentTheme === THEME_LIGHT) { + icon.textContent = '☾'; + button.title = 'Theme: Light (click for Dark)'; + } else { + icon.textContent = '☀'; + button.title = 'Theme: Dark (click for Auto)'; + } + } + + function cycleTheme() { + var current = getStoredTheme(); + var next; + + // Cycle: auto -> light -> dark -> auto + if (current === THEME_AUTO) { + next = THEME_LIGHT; + } else if (current === THEME_LIGHT) { + next = THEME_DARK; + } else { + next = THEME_AUTO; + } + + setStoredTheme(next); + applyTheme(next); + } + + // Initialize theme on page load + function initTheme() { + var stored = getStoredTheme(); + applyTheme(stored); + + // Listen for system theme changes + if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: dark)').addListener(function() { + var currentStored = getStoredTheme(); + if (currentStored === THEME_AUTO) { + applyTheme(THEME_AUTO); + } + }); + } + + // Set up toggle button click handler + var button = document.getElementById('theme-toggle'); + if (button) { + button.addEventListener('click', function(e) { + e.preventDefault(); + cycleTheme(); + }); + } + } + + // Run on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initTheme); + } else { + initTheme(); + } +})(); diff --git a/docs/v3/source/layouts/layout.erb b/docs/v3/source/layouts/layout.erb index 029bdb4807..288c3bd11b 100644 --- a/docs/v3/source/layouts/layout.erb +++ b/docs/v3/source/layouts/layout.erb @@ -53,6 +53,10 @@ table_of_contents = extract_table_of_contents(html)