diff --git a/3rdparty/jquery.tooltipster.js b/3rdparty/jquery.tooltipster.js new file mode 100644 index 0000000..16e1f81 --- /dev/null +++ b/3rdparty/jquery.tooltipster.js @@ -0,0 +1,1282 @@ +/* + +Tooltipster 3.2.6 | 2014-07-16 +A rockin' custom tooltip jQuery plugin + +Developed by Caleb Jacob under the MIT license http://opensource.org/licenses/MIT + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +;(function ($, window, document) { + + var pluginName = "tooltipster", + defaults = { + animation: 'fade', + arrow: true, + arrowColor: '', + autoClose: true, + content: null, + contentAsHTML: false, + contentCloning: true, + debug: true, + delay: 200, + minWidth: 0, + maxWidth: null, + functionInit: function(origin, content) {}, + functionBefore: function(origin, continueTooltip) { + continueTooltip(); + }, + functionReady: function(origin, tooltip) {}, + functionAfter: function(origin) {}, + icon: '(?)', + iconCloning: true, + iconDesktop: false, + iconTouch: false, + iconTheme: 'tooltipster-icon', + interactive: false, + interactiveTolerance: 350, + multiple: false, + offsetX: 0, + offsetY: 0, + onlyOne: false, + position: 'top', + positionTracker: false, + speed: 350, + timer: 0, + theme: 'tooltipster-default', + touchDevices: true, + trigger: 'hover', + updateAnimation: true + }; + + function Plugin(element, options) { + + // list of instance variables + + this.bodyOverflowX; + // stack of custom callbacks provided as parameters to API methods + this.callbacks = { + hide: [], + show: [] + }; + this.checkInterval = null; + // this will be the user content shown in the tooltip. A capital "C" is used because there is also a method called content() + this.Content; + // this is the original element which is being applied the tooltipster plugin + this.$el = $(element); + // this will be the element which triggers the appearance of the tooltip on hover/click/custom events. + // it will be the same as this.$el if icons are not used (see in the options), otherwise it will correspond to the created icon + this.$elProxy; + this.elProxyPosition; + this.enabled = true; + this.options = $.extend({}, defaults, options); + this.mouseIsOverProxy = false; + // a unique namespace per instance, for easy selective unbinding + this.namespace = 'tooltipster-'+ Math.round(Math.random()*100000); + // Status (capital S) can be either : appearing, shown, disappearing, hidden + this.Status = 'hidden'; + this.timerHide = null; + this.timerShow = null; + // this will be the tooltip element (jQuery wrapped HTML element) + this.$tooltip; + + // for backward compatibility + this.options.iconTheme = this.options.iconTheme.replace('.', ''); + this.options.theme = this.options.theme.replace('.', ''); + + // launch + + this._init(); + } + + Plugin.prototype = { + + _init: function() { + + var self = this; + + // disable the plugin on old browsers (including IE7 and lower) + if (document.querySelector) { + + // note : the content is null (empty) by default and can stay that way if the plugin remains initialized but not fed any content. The tooltip will just not appear. + + // if content is provided in the options, its has precedence over the title attribute. Remark : an empty string is considered content, only 'null' represents the absence of content. + if (self.options.content !== null){ + self._content_set(self.options.content); + } + else { + // the same remark as above applies : empty strings (like title="") are considered content and will be shown. Do not define any attribute at all if you want to initialize the plugin without content at start. + var t = self.$el.attr('title'); + if(typeof t === 'undefined') t = null; + + self._content_set(t); + } + + var c = self.options.functionInit.call(self.$el, self.$el, self.Content); + if(typeof c !== 'undefined') self._content_set(c); + + self.$el + // strip the title off of the element to prevent the default tooltips from popping up + .removeAttr('title') + // to be able to find all instances on the page later (upon window events in particular) + .addClass('tooltipstered'); + + // detect if we're changing the tooltip origin to an icon + // note about this condition : if the device has touch capability and self.options.iconTouch is false, you'll have no icons event though you may consider your device as a desktop if it also has a mouse. Not sure why someone would have this use case though. + if ((!deviceHasTouchCapability && self.options.iconDesktop) || (deviceHasTouchCapability && self.options.iconTouch)) { + + // TODO : the tooltip should be automatically be given an absolute position to be near the origin. Otherwise, when the origin is floating or what, it's going to be nowhere near it and disturb the position flow of the page elements. It will imply that the icon also detects when its origin moves, to follow it : not trivial. + // Until it's done, the icon feature does not really make sense since the user still has most of the work to do by himself + + // if the icon provided is in the form of a string + if(typeof self.options.icon === 'string'){ + // wrap it in a span with the icon class + self.$elProxy = $(''); + self.$elProxy.text(self.options.icon); + } + // if it is an object (sensible choice) + else { + // (deep) clone the object if iconCloning == true, to make sure every instance has its own proxy. We use the icon without wrapping, no need to. We do not give it a class either, as the user will undoubtedly style the object on his own and since our css properties may conflict with his own + if (self.options.iconCloning) self.$elProxy = self.options.icon.clone(true); + else self.$elProxy = self.options.icon; + } + + self.$elProxy.insertAfter(self.$el); + } + else { + self.$elProxy = self.$el; + } + + // for 'click' and 'hover' triggers : bind on events to open the tooltip. Closing is now handled in _showNow() because of its bindings. + // Notes about touch events : + // - mouseenter, mouseleave and clicks happen even on pure touch devices because they are emulated. deviceIsPureTouch() is a simple attempt to detect them. + // - on hybrid devices, we do not prevent touch gesture from opening tooltips. It would be too complex to differentiate real mouse events from emulated ones. + // - we check deviceIsPureTouch() at each event rather than prior to binding because the situation may change during browsing + if (self.options.trigger == 'hover') { + + // these binding are for mouse interaction only + self.$elProxy + .on('mouseenter.'+ self.namespace, function() { + if (!deviceIsPureTouch() || self.options.touchDevices) { + self.mouseIsOverProxy = true; + self._show(); + } + }) + .on('mouseleave.'+ self.namespace, function() { + if (!deviceIsPureTouch() || self.options.touchDevices) { + self.mouseIsOverProxy = false; + } + }); + + // for touch interaction only + if (deviceHasTouchCapability && self.options.touchDevices) { + + // for touch devices, we immediately display the tooltip because we cannot rely on mouseleave to handle the delay + self.$elProxy.on('touchstart.'+ self.namespace, function() { + self._showNow(); + }); + } + } + else if (self.options.trigger == 'click') { + + // note : for touch devices, we do not bind on touchstart, we only rely on the emulated clicks (triggered by taps) + self.$elProxy.on('click.'+ self.namespace, function() { + if (!deviceIsPureTouch() || self.options.touchDevices) { + self._show(); + } + }); + } + } + }, + + // this function will schedule the opening of the tooltip after the delay, if there is one + _show: function() { + + var self = this; + + if (self.Status != 'shown' && self.Status != 'appearing') { + + if (self.options.delay) { + self.timerShow = setTimeout(function(){ + + // for hover trigger, we check if the mouse is still over the proxy, otherwise we do not show anything + if (self.options.trigger == 'click' || (self.options.trigger == 'hover' && self.mouseIsOverProxy)) { + self._showNow(); + } + }, self.options.delay); + } + else self._showNow(); + } + }, + + // this function will open the tooltip right away + _showNow: function(callback) { + + var self = this; + + // call our constructor custom function before continuing + self.options.functionBefore.call(self.$el, self.$el, function() { + + // continue only if the tooltip is enabled and has any content + if (self.enabled && self.Content !== null) { + + // save the method callback and cancel hide method callbacks + if (callback) self.callbacks.show.push(callback); + self.callbacks.hide = []; + + //get rid of any appearance timer + clearTimeout(self.timerShow); + self.timerShow = null; + clearTimeout(self.timerHide); + self.timerHide = null; + + // if we only want one tooltip open at a time, close all auto-closing tooltips currently open and not already disappearing + if (self.options.onlyOne) { + $('.tooltipstered').not(self.$el).each(function(i,el) { + + var $el = $(el), + nss = $el.data('tooltipster-ns'); + + // iterate on all tooltips of the element + $.each(nss, function(i, ns){ + var instance = $el.data(ns), + // we have to use the public methods here + s = instance.status(), + ac = instance.option('autoClose'); + + if (s !== 'hidden' && s !== 'disappearing' && ac) { + instance.hide(); + } + }); + }); + } + + var finish = function() { + self.Status = 'shown'; + + // trigger any show method custom callbacks and reset them + $.each(self.callbacks.show, function(i,c) { c.call(self.$el); }); + self.callbacks.show = []; + }; + + // if this origin already has its tooltip open + if (self.Status !== 'hidden') { + + // the timer (if any) will start (or restart) right now + var extraTime = 0; + + // if it was disappearing, cancel that + if (self.Status === 'disappearing') { + + self.Status = 'appearing'; + + if (supportsTransitions()) { + + self.$tooltip + .clearQueue() + .removeClass('tooltipster-dying') + .addClass('tooltipster-'+ self.options.animation +'-show'); + + if (self.options.speed > 0) self.$tooltip.delay(self.options.speed); + + self.$tooltip.queue(finish); + } + else { + // in case the tooltip was currently fading out, bring it back to life + self.$tooltip + .stop() + .fadeIn(finish); + } + } + // if the tooltip is already open, we still need to trigger the method custom callback + else if(self.Status === 'shown') { + finish(); + } + } + // if the tooltip isn't already open, open that sucker up! + else { + + self.Status = 'appearing'; + + // the timer (if any) will start when the tooltip has fully appeared after its transition + var extraTime = self.options.speed; + + // disable horizontal scrollbar to keep overflowing tooltips from jacking with it and then restore it to its previous value + self.bodyOverflowX = $('body').css('overflow-x'); + $('body').css('overflow-x', 'hidden'); + + // get some other settings related to building the tooltip + var animation = 'tooltipster-' + self.options.animation, + animationSpeed = '-webkit-transition-duration: '+ self.options.speed +'ms; -webkit-animation-duration: '+ self.options.speed +'ms; -moz-transition-duration: '+ self.options.speed +'ms; -moz-animation-duration: '+ self.options.speed +'ms; -o-transition-duration: '+ self.options.speed +'ms; -o-animation-duration: '+ self.options.speed +'ms; -ms-transition-duration: '+ self.options.speed +'ms; -ms-animation-duration: '+ self.options.speed +'ms; transition-duration: '+ self.options.speed +'ms; animation-duration: '+ self.options.speed +'ms;', + minWidth = self.options.minWidth ? 'min-width:'+ Math.round(self.options.minWidth) +'px;' : '', + maxWidth = self.options.maxWidth ? 'max-width:'+ Math.round(self.options.maxWidth) +'px;' : '', + pointerEvents = self.options.interactive ? 'pointer-events: auto;' : ''; + + // build the base of our tooltip + self.$tooltip = $('
'); + + // only add the animation class if the user has a browser that supports animations + if (supportsTransitions()) self.$tooltip.addClass(animation); + + // insert the content + self._content_insert(); + + // attach + self.$tooltip.appendTo('body'); + + // do all the crazy calculations and positioning + self.reposition(); + + // call our custom callback since the content of the tooltip is now part of the DOM + self.options.functionReady.call(self.$el, self.$el, self.$tooltip); + + // animate in the tooltip + if (supportsTransitions()) { + + self.$tooltip.addClass(animation + '-show'); + + if(self.options.speed > 0) self.$tooltip.delay(self.options.speed); + + self.$tooltip.queue(finish); + } + else { + self.$tooltip.css('display', 'none').fadeIn(self.options.speed, finish); + } + + // will check if our tooltip origin is removed while the tooltip is shown + self._interval_set(); + + // reposition on scroll (otherwise position:fixed element's tooltips will move away form their origin) and on resize (in case position can/has to be changed) + $(window).on('scroll.'+ self.namespace +' resize.'+ self.namespace, function() { + self.reposition(); + }); + + // auto-close bindings + if (self.options.autoClose) { + + // in case a listener is already bound for autoclosing (mouse or touch, hover or click), unbind it first + $('body').off('.'+ self.namespace); + + // here we'll have to set different sets of bindings for both touch and mouse + if (self.options.trigger == 'hover') { + + // if the user touches the body, hide + if (deviceHasTouchCapability) { + // timeout 0 : explanation below in click section + setTimeout(function() { + // we don't want to bind on click here because the initial touchstart event has not yet triggered its click event, which is thus about to happen + $('body').on('touchstart.'+ self.namespace, function() { + self.hide(); + }); + }, 0); + } + + // if we have to allow interaction + if (self.options.interactive) { + + // touch events inside the tooltip must not close it + if (deviceHasTouchCapability) { + self.$tooltip.on('touchstart.'+ self.namespace, function(event) { + event.stopPropagation(); + }); + } + + // as for mouse interaction, we get rid of the tooltip only after the mouse has spent some time out of it + var tolerance = null; + + self.$elProxy.add(self.$tooltip) + // hide after some time out of the proxy and the tooltip + .on('mouseleave.'+ self.namespace + '-autoClose', function() { + clearTimeout(tolerance); + tolerance = setTimeout(function(){ + self.hide(); + }, self.options.interactiveTolerance); + }) + // suspend timeout when the mouse is over the proxy or the tooltip + .on('mouseenter.'+ self.namespace + '-autoClose', function() { + clearTimeout(tolerance); + }); + } + // if this is a non-interactive tooltip, get rid of it if the mouse leaves + else { + self.$elProxy.on('mouseleave.'+ self.namespace + '-autoClose', function() { + self.hide(); + }); + } + } + // here we'll set the same bindings for both clicks and touch on the body to hide the tooltip + else if(self.options.trigger == 'click'){ + + // use a timeout to prevent immediate closing if the method was called on a click event and if options.delay == 0 (because of bubbling) + setTimeout(function() { + $('body').on('click.'+ self.namespace +' touchstart.'+ self.namespace, function() { + self.hide(); + }); + }, 0); + + // if interactive, we'll stop the events that were emitted from inside the tooltip to stop autoClosing + if (self.options.interactive) { + + // note : the touch events will just not be used if the plugin is not enabled on touch devices + self.$tooltip.on('click.'+ self.namespace +' touchstart.'+ self.namespace, function(event) { + event.stopPropagation(); + }); + } + } + } + } + + // if we have a timer set, let the countdown begin + if (self.options.timer > 0) { + + self.timerHide = setTimeout(function() { + self.timerHide = null; + self.hide(); + }, self.options.timer + extraTime); + } + } + }); + }, + + _interval_set: function() { + + var self = this; + + self.checkInterval = setInterval(function() { + + // if the tooltip and/or its interval should be stopped + if ( + // if the origin has been removed + $('body').find(self.$el).length === 0 + // if the elProxy has been removed + || $('body').find(self.$elProxy).length === 0 + // if the tooltip has been closed + || self.Status == 'hidden' + // if the tooltip has somehow been removed + || $('body').find(self.$tooltip).length === 0 + ) { + // remove the tooltip if it's still here + if (self.Status == 'shown' || self.Status == 'appearing') self.hide(); + + // clear this interval as it is no longer necessary + self._interval_cancel(); + } + // if everything is alright + else { + // compare the former and current positions of the elProxy to reposition the tooltip if need be + if(self.options.positionTracker){ + + var p = self._repositionInfo(self.$elProxy), + identical = false; + + // compare size first (a change requires repositioning too) + if(areEqual(p.dimension, self.elProxyPosition.dimension)){ + + // for elements with a fixed position, we track the top and left properties (relative to window) + if(self.$elProxy.css('position') === 'fixed'){ + if(areEqual(p.position, self.elProxyPosition.position)) identical = true; + } + // otherwise, track total offset (relative to document) + else { + if(areEqual(p.offset, self.elProxyPosition.offset)) identical = true; + } + } + + if(!identical){ + self.reposition(); + } + } + } + }, 200); + }, + + _interval_cancel: function() { + clearInterval(this.checkInterval); + // clean delete + this.checkInterval = null; + }, + + _content_set: function(content) { + // clone if asked. Cloning the object makes sure that each instance has its own version of the content (in case a same object were provided for several instances) + // reminder : typeof null === object + if (typeof content === 'object' && content !== null && this.options.contentCloning) { + content = content.clone(true); + } + this.Content = content; + }, + + _content_insert: function() { + + var self = this, + $d = this.$tooltip.find('.tooltipster-content'); + + if (typeof self.Content === 'string' && !self.options.contentAsHTML) { + $d.text(self.Content); + } + else { + $d + .empty() + .append(self.Content); + } + }, + + _update: function(content) { + + var self = this; + + // change the content + self._content_set(content); + + if (self.Content !== null) { + + // update the tooltip if it is open + if (self.Status !== 'hidden') { + + // reset the content in the tooltip + self._content_insert(); + + // reposition and resize the tooltip + self.reposition(); + + // if we want to play a little animation showing the content changed + if (self.options.updateAnimation) { + + if (supportsTransitions()) { + + self.$tooltip.css({ + 'width': '', + '-webkit-transition': 'all ' + self.options.speed + 'ms, width 0ms, height 0ms, left 0ms, top 0ms', + '-moz-transition': 'all ' + self.options.speed + 'ms, width 0ms, height 0ms, left 0ms, top 0ms', + '-o-transition': 'all ' + self.options.speed + 'ms, width 0ms, height 0ms, left 0ms, top 0ms', + '-ms-transition': 'all ' + self.options.speed + 'ms, width 0ms, height 0ms, left 0ms, top 0ms', + 'transition': 'all ' + self.options.speed + 'ms, width 0ms, height 0ms, left 0ms, top 0ms' + }).addClass('tooltipster-content-changing'); + + // reset the CSS transitions and finish the change animation + setTimeout(function() { + + if(self.Status != 'hidden'){ + + self.$tooltip.removeClass('tooltipster-content-changing'); + + // after the changing animation has completed, reset the CSS transitions + setTimeout(function() { + + if(self.Status !== 'hidden'){ + self.$tooltip.css({ + '-webkit-transition': self.options.speed + 'ms', + '-moz-transition': self.options.speed + 'ms', + '-o-transition': self.options.speed + 'ms', + '-ms-transition': self.options.speed + 'ms', + 'transition': self.options.speed + 'ms' + }); + } + }, self.options.speed); + } + }, self.options.speed); + } + else { + self.$tooltip.fadeTo(self.options.speed, 0.5, function() { + if(self.Status != 'hidden'){ + self.$tooltip.fadeTo(self.options.speed, 1); + } + }); + } + } + } + } + else { + self.hide(); + } + }, + + _repositionInfo: function($el) { + return { + dimension: { + height: $el.outerHeight(false), + width: $el.outerWidth(false) + }, + offset: $el.offset(), + position: { + left: parseInt($el.css('left')), + top: parseInt($el.css('top')) + } + }; + }, + + hide: function(callback) { + + var self = this; + + // save the method custom callback and cancel any show method custom callbacks + if (callback) self.callbacks.hide.push(callback); + self.callbacks.show = []; + + // get rid of any appearance timeout + clearTimeout(self.timerShow); + self.timerShow = null; + clearTimeout(self.timerHide); + self.timerHide = null; + + var finishCallbacks = function() { + // trigger any hide method custom callbacks and reset them + $.each(self.callbacks.hide, function(i,c) { c.call(self.$el); }); + self.callbacks.hide = []; + }; + + // hide + if (self.Status == 'shown' || self.Status == 'appearing') { + + self.Status = 'disappearing'; + + var finish = function() { + + self.Status = 'hidden'; + + // detach our content object first, so the next jQuery's remove() call does not unbind its event handlers + if (typeof self.Content == 'object' && self.Content !== null) { + self.Content.detach(); + } + + self.$tooltip.remove(); + self.$tooltip = null; + + // unbind orientationchange, scroll and resize listeners + $(window).off('.'+ self.namespace); + + $('body') + // unbind any auto-closing click/touch listeners + .off('.'+ self.namespace) + .css('overflow-x', self.bodyOverflowX); + + // unbind any auto-closing click/touch listeners + $('body').off('.'+ self.namespace); + + // unbind any auto-closing hover listeners + self.$elProxy.off('.'+ self.namespace + '-autoClose'); + + // call our constructor custom callback function + self.options.functionAfter.call(self.$el, self.$el); + + // call our method custom callbacks functions + finishCallbacks(); + }; + + if (supportsTransitions()) { + + self.$tooltip + .clearQueue() + .removeClass('tooltipster-' + self.options.animation + '-show') + // for transitions only + .addClass('tooltipster-dying'); + + if(self.options.speed > 0) self.$tooltip.delay(self.options.speed); + + self.$tooltip.queue(finish); + } + else { + self.$tooltip + .stop() + .fadeOut(self.options.speed, finish); + } + } + // if the tooltip is already hidden, we still need to trigger the method custom callback + else if(self.Status == 'hidden') { + finishCallbacks(); + } + + return self; + }, + + // the public show() method is actually an alias for the private showNow() method + show: function(callback) { + this._showNow(callback); + return this; + }, + + // 'update' is deprecated in favor of 'content' but is kept for backward compatibility + update: function(c) { + return this.content(c); + }, + content: function(c) { + // getter method + if(typeof c === 'undefined'){ + return this.Content; + } + // setter method + else { + this._update(c); + return this; + } + }, + + reposition: function() { + + var self = this; + + // in case the tooltip has been removed from DOM manually + if ($('body').find(self.$tooltip).length !== 0) { + + // reset width + self.$tooltip.css('width', ''); + + // find variables to determine placement + self.elProxyPosition = self._repositionInfo(self.$elProxy); + var arrowReposition = null, + windowWidth = $(window).width(), + // shorthand + proxy = self.elProxyPosition, + tooltipWidth = self.$tooltip.outerWidth(false), + tooltipInnerWidth = self.$tooltip.innerWidth() + 1, // this +1 stops FireFox from sometimes forcing an additional text line + tooltipHeight = self.$tooltip.outerHeight(false); + + // if this is an tag inside a , all hell breaks loose. Recalculate all the measurements based on coordinates + if (self.$elProxy.is('area')) { + var areaShape = self.$elProxy.attr('shape'), + mapName = self.$elProxy.parent().attr('name'), + map = $('img[usemap="#'+ mapName +'"]'), + mapOffsetLeft = map.offset().left, + mapOffsetTop = map.offset().top, + areaMeasurements = self.$elProxy.attr('coords') !== undefined ? self.$elProxy.attr('coords').split(',') : undefined; + + if (areaShape == 'circle') { + var areaLeft = parseInt(areaMeasurements[0]), + areaTop = parseInt(areaMeasurements[1]), + areaWidth = parseInt(areaMeasurements[2]); + proxy.dimension.height = areaWidth * 2; + proxy.dimension.width = areaWidth * 2; + proxy.offset.top = mapOffsetTop + areaTop - areaWidth; + proxy.offset.left = mapOffsetLeft + areaLeft - areaWidth; + } + else if (areaShape == 'rect') { + var areaLeft = parseInt(areaMeasurements[0]), + areaTop = parseInt(areaMeasurements[1]), + areaRight = parseInt(areaMeasurements[2]), + areaBottom = parseInt(areaMeasurements[3]); + proxy.dimension.height = areaBottom - areaTop; + proxy.dimension.width = areaRight - areaLeft; + proxy.offset.top = mapOffsetTop + areaTop; + proxy.offset.left = mapOffsetLeft + areaLeft; + } + else if (areaShape == 'poly') { + var areaXs = [], + areaYs = [], + areaSmallestX = 0, + areaSmallestY = 0, + areaGreatestX = 0, + areaGreatestY = 0, + arrayAlternate = 'even'; + + for (var i = 0; i < areaMeasurements.length; i++) { + var areaNumber = parseInt(areaMeasurements[i]); + + if (arrayAlternate == 'even') { + if (areaNumber > areaGreatestX) { + areaGreatestX = areaNumber; + if (i === 0) { + areaSmallestX = areaGreatestX; + } + } + + if (areaNumber < areaSmallestX) { + areaSmallestX = areaNumber; + } + + arrayAlternate = 'odd'; + } + else { + if (areaNumber > areaGreatestY) { + areaGreatestY = areaNumber; + if (i == 1) { + areaSmallestY = areaGreatestY; + } + } + + if (areaNumber < areaSmallestY) { + areaSmallestY = areaNumber; + } + + arrayAlternate = 'even'; + } + } + + proxy.dimension.height = areaGreatestY - areaSmallestY; + proxy.dimension.width = areaGreatestX - areaSmallestX; + proxy.offset.top = mapOffsetTop + areaSmallestY; + proxy.offset.left = mapOffsetLeft + areaSmallestX; + } + else { + proxy.dimension.height = map.outerHeight(false); + proxy.dimension.width = map.outerWidth(false); + proxy.offset.top = mapOffsetTop; + proxy.offset.left = mapOffsetLeft; + } + } + + // our function and global vars for positioning our tooltip + var myLeft = 0, + myLeftMirror = 0, + myTop = 0, + offsetY = parseInt(self.options.offsetY), + offsetX = parseInt(self.options.offsetX), + // this is the arrow position that will eventually be used. It may differ from the position option if the tooltip cannot be displayed in this position + practicalPosition = self.options.position; + + // a function to detect if the tooltip is going off the screen horizontally. If so, reposition the crap out of it! + function dontGoOffScreenX() { + + var windowLeft = $(window).scrollLeft(); + + // if the tooltip goes off the left side of the screen, line it up with the left side of the window + if((myLeft - windowLeft) < 0) { + arrowReposition = myLeft - windowLeft; + myLeft = windowLeft; + } + + // if the tooltip goes off the right of the screen, line it up with the right side of the window + if (((myLeft + tooltipWidth) - windowLeft) > windowWidth) { + arrowReposition = myLeft - ((windowWidth + windowLeft) - tooltipWidth); + myLeft = (windowWidth + windowLeft) - tooltipWidth; + } + } + + // a function to detect if the tooltip is going off the screen vertically. If so, switch to the opposite! + function dontGoOffScreenY(switchTo, switchFrom) { + // if it goes off the top off the page + if(((proxy.offset.top - $(window).scrollTop() - tooltipHeight - offsetY - 12) < 0) && (switchFrom.indexOf('top') > -1)) { + practicalPosition = switchTo; + } + + // if it goes off the bottom of the page + if (((proxy.offset.top + proxy.dimension.height + tooltipHeight + 12 + offsetY) > ($(window).scrollTop() + $(window).height())) && (switchFrom.indexOf('bottom') > -1)) { + practicalPosition = switchTo; + myTop = (proxy.offset.top - tooltipHeight) - offsetY - 12; + } + } + + if(practicalPosition == 'top') { + var leftDifference = (proxy.offset.left + tooltipWidth) - (proxy.offset.left + proxy.dimension.width); + myLeft = (proxy.offset.left + offsetX) - (leftDifference / 2); + myTop = (proxy.offset.top - tooltipHeight) - offsetY - 12; + dontGoOffScreenX(); + dontGoOffScreenY('bottom', 'top'); + } + + if(practicalPosition == 'top-left') { + myLeft = proxy.offset.left + offsetX; + myTop = (proxy.offset.top - tooltipHeight) - offsetY - 12; + dontGoOffScreenX(); + dontGoOffScreenY('bottom-left', 'top-left'); + } + + if(practicalPosition == 'top-right') { + myLeft = (proxy.offset.left + proxy.dimension.width + offsetX) - tooltipWidth; + myTop = (proxy.offset.top - tooltipHeight) - offsetY - 12; + dontGoOffScreenX(); + dontGoOffScreenY('bottom-right', 'top-right'); + } + + if(practicalPosition == 'bottom') { + var leftDifference = (proxy.offset.left + tooltipWidth) - (proxy.offset.left + proxy.dimension.width); + myLeft = proxy.offset.left - (leftDifference / 2) + offsetX; + myTop = (proxy.offset.top + proxy.dimension.height) + offsetY + 12; + dontGoOffScreenX(); + dontGoOffScreenY('top', 'bottom'); + } + + if(practicalPosition == 'bottom-left') { + myLeft = proxy.offset.left + offsetX; + myTop = (proxy.offset.top + proxy.dimension.height) + offsetY + 12; + dontGoOffScreenX(); + dontGoOffScreenY('top-left', 'bottom-left'); + } + + if(practicalPosition == 'bottom-right') { + myLeft = (proxy.offset.left + proxy.dimension.width + offsetX) - tooltipWidth; + myTop = (proxy.offset.top + proxy.dimension.height) + offsetY + 12; + dontGoOffScreenX(); + dontGoOffScreenY('top-right', 'bottom-right'); + } + + if(practicalPosition == 'left') { + myLeft = proxy.offset.left - offsetX - tooltipWidth - 12; + myLeftMirror = proxy.offset.left + offsetX + proxy.dimension.width + 12; + var topDifference = (proxy.offset.top + tooltipHeight) - (proxy.offset.top + proxy.dimension.height); + myTop = proxy.offset.top - (topDifference / 2) - offsetY; + + // if the tooltip goes off boths sides of the page + if((myLeft < 0) && ((myLeftMirror + tooltipWidth) > windowWidth)) { + var borderWidth = parseFloat(self.$tooltip.css('border-width')) * 2, + newWidth = (tooltipWidth + myLeft) - borderWidth; + self.$tooltip.css('width', newWidth + 'px'); + + tooltipHeight = self.$tooltip.outerHeight(false); + myLeft = proxy.offset.left - offsetX - newWidth - 12 - borderWidth; + topDifference = (proxy.offset.top + tooltipHeight) - (proxy.offset.top + proxy.dimension.height); + myTop = proxy.offset.top - (topDifference / 2) - offsetY; + } + + // if it only goes off one side, flip it to the other side + else if(myLeft < 0) { + myLeft = proxy.offset.left + offsetX + proxy.dimension.width + 12; + arrowReposition = 'left'; + } + } + + if(practicalPosition == 'right') { + myLeft = proxy.offset.left + offsetX + proxy.dimension.width + 12; + myLeftMirror = proxy.offset.left - offsetX - tooltipWidth - 12; + var topDifference = (proxy.offset.top + tooltipHeight) - (proxy.offset.top + proxy.dimension.height); + myTop = proxy.offset.top - (topDifference / 2) - offsetY; + + // if the tooltip goes off boths sides of the page + if(((myLeft + tooltipWidth) > windowWidth) && (myLeftMirror < 0)) { + var borderWidth = parseFloat(self.$tooltip.css('border-width')) * 2, + newWidth = (windowWidth - myLeft) - borderWidth; + self.$tooltip.css('width', newWidth + 'px'); + + tooltipHeight = self.$tooltip.outerHeight(false); + topDifference = (proxy.offset.top + tooltipHeight) - (proxy.offset.top + proxy.dimension.height); + myTop = proxy.offset.top - (topDifference / 2) - offsetY; + } + + // if it only goes off one side, flip it to the other side + else if((myLeft + tooltipWidth) > windowWidth) { + myLeft = proxy.offset.left - offsetX - tooltipWidth - 12; + arrowReposition = 'right'; + } + } + + // if arrow is set true, style it and append it + if (self.options.arrow) { + + var arrowClass = 'tooltipster-arrow-' + practicalPosition; + + // set color of the arrow + if(self.options.arrowColor.length < 1) { + var arrowColor = self.$tooltip.css('background-color'); + } + else { + var arrowColor = self.options.arrowColor; + } + + // if the tooltip was going off the page and had to re-adjust, we need to update the arrow's position + if (!arrowReposition) { + arrowReposition = ''; + } + else if (arrowReposition == 'left') { + arrowClass = 'tooltipster-arrow-right'; + arrowReposition = ''; + } + else if (arrowReposition == 'right') { + arrowClass = 'tooltipster-arrow-left'; + arrowReposition = ''; + } + else { + arrowReposition = 'left:'+ Math.round(arrowReposition) +'px;'; + } + + // building the logic to create the border around the arrow of the tooltip + if ((practicalPosition == 'top') || (practicalPosition == 'top-left') || (practicalPosition == 'top-right')) { + var tooltipBorderWidth = parseFloat(self.$tooltip.css('border-bottom-width')), + tooltipBorderColor = self.$tooltip.css('border-bottom-color'); + } + else if ((practicalPosition == 'bottom') || (practicalPosition == 'bottom-left') || (practicalPosition == 'bottom-right')) { + var tooltipBorderWidth = parseFloat(self.$tooltip.css('border-top-width')), + tooltipBorderColor = self.$tooltip.css('border-top-color'); + } + else if (practicalPosition == 'left') { + var tooltipBorderWidth = parseFloat(self.$tooltip.css('border-right-width')), + tooltipBorderColor = self.$tooltip.css('border-right-color'); + } + else if (practicalPosition == 'right') { + var tooltipBorderWidth = parseFloat(self.$tooltip.css('border-left-width')), + tooltipBorderColor = self.$tooltip.css('border-left-color'); + } + else { + var tooltipBorderWidth = parseFloat(self.$tooltip.css('border-bottom-width')), + tooltipBorderColor = self.$tooltip.css('border-bottom-color'); + } + + if (tooltipBorderWidth > 1) { + tooltipBorderWidth++; + } + + var arrowBorder = ''; + if (tooltipBorderWidth !== 0) { + var arrowBorderSize = '', + arrowBorderColor = 'border-color: '+ tooltipBorderColor +';'; + if (arrowClass.indexOf('bottom') !== -1) { + arrowBorderSize = 'margin-top: -'+ Math.round(tooltipBorderWidth) +'px;'; + } + else if (arrowClass.indexOf('top') !== -1) { + arrowBorderSize = 'margin-bottom: -'+ Math.round(tooltipBorderWidth) +'px;'; + } + else if (arrowClass.indexOf('left') !== -1) { + arrowBorderSize = 'margin-right: -'+ Math.round(tooltipBorderWidth) +'px;'; + } + else if (arrowClass.indexOf('right') !== -1) { + arrowBorderSize = 'margin-left: -'+ Math.round(tooltipBorderWidth) +'px;'; + } + arrowBorder = ''; + } + + // if the arrow already exists, remove and replace it + self.$tooltip.find('.tooltipster-arrow').remove(); + + // build out the arrow and append it + var arrowConstruct = '
'+ arrowBorder +'
'; + self.$tooltip.append(arrowConstruct); + } + + // position the tooltip + self.$tooltip.css({'top': Math.round(myTop) + 'px', 'left': Math.round(myLeft) + 'px'}); + } + + return self; + }, + + enable: function() { + this.enabled = true; + return this; + }, + + disable: function() { + // hide first, in case the tooltip would not disappear on its own (autoClose false) + this.hide(); + this.enabled = false; + return this; + }, + + destroy: function() { + + var self = this; + + self.hide(); + + // remove the icon, if any + if(self.$el[0] !== self.$elProxy[0]) self.$elProxy.remove(); + + self.$el + .removeData(self.namespace) + .off('.'+ self.namespace); + + var ns = self.$el.data('tooltipster-ns'); + + // if there are no more tooltips on this element + if(ns.length === 1){ + + // old school technique when outerHTML is not supported + var stringifiedContent = (typeof self.Content === 'string') ? self.Content : $('
').append(self.Content).html(); + + self.$el + .removeClass('tooltipstered') + .attr('title', stringifiedContent) + .removeData(self.namespace) + .removeData('tooltipster-ns') + .off('.'+ self.namespace); + } + else { + // remove the instance namespace from the list of namespaces of tooltips present on the element + ns = $.grep(ns, function(el, i){ + return el !== self.namespace; + }); + self.$el.data('tooltipster-ns', ns); + } + + return self; + }, + + elementIcon: function() { + return (this.$el[0] !== this.$elProxy[0]) ? this.$elProxy[0] : undefined; + }, + + elementTooltip: function() { + return this.$tooltip ? this.$tooltip[0] : undefined; + }, + + // public methods but for internal use only + // getter if val is ommitted, setter otherwise + option: function(o, val) { + if (typeof val == 'undefined') return this.options[o]; + else { + this.options[o] = val; + return this; + } + }, + status: function() { + return this.Status; + } + }; + + $.fn[pluginName] = function () { + + // for using in closures + var args = arguments; + + // if we are not in the context of jQuery wrapped HTML element(s) : + // this happens when calling static methods in the form $.fn.tooltipster('methodName'), or when calling $(sel).tooltipster('methodName or options') where $(sel) does not match anything + if (this.length === 0) { + + // if the first argument is a method name + if (typeof args[0] === 'string') { + + var methodIsStatic = true; + + // list static methods here (usable by calling $.fn.tooltipster('methodName');) + switch (args[0]) { + + case 'setDefaults': + // change default options for all future instances + $.extend(defaults, args[1]); + break; + + default: + methodIsStatic = false; + break; + } + + // $.fn.tooltipster('methodName') calls will return true + if (methodIsStatic) return true; + // $(sel).tooltipster('methodName') calls will return the list of objects event though it's empty because chaining should work on empty lists + else return this; + } + // the first argument is undefined or an object of options : we are initalizing but there is no element matched by selector + else { + // still chainable : same as above + return this; + } + } + // this happens when calling $(sel).tooltipster('methodName or options') where $(sel) matches one or more elements + else { + + // method calls + if (typeof args[0] === 'string') { + + var v = '#*$~&'; + + this.each(function() { + + // retrieve the namepaces of the tooltip(s) that exist on that element. We will interact with the first tooltip only. + var ns = $(this).data('tooltipster-ns'), + // self represents the instance of the first tooltipster plugin associated to the current HTML object of the loop + self = ns ? $(this).data(ns[0]) : null; + + // if the current element holds a tooltipster instance + if (self) { + + if (typeof self[args[0]] === 'function') { + // note : args[1] and args[2] may not be defined + var resp = self[args[0]](args[1], args[2]); + } + else { + throw new Error('Unknown method .tooltipster("' + args[0] + '")'); + } + + // if the function returned anything other than the instance itself (which implies chaining) + if (resp !== self){ + v = resp; + // return false to stop .each iteration on the first element matched by the selector + return false; + } + } + else { + throw new Error('You called Tooltipster\'s "' + args[0] + '" method on an uninitialized element'); + } + }); + + return (v !== '#*$~&') ? v : this; + } + // first argument is undefined or an object : the tooltip is initializing + else { + + var instances = [], + // is there a defined value for the multiple option in the options object ? + multipleIsSet = args[0] && typeof args[0].multiple !== 'undefined', + // if the multiple option is set to true, or if it's not defined but set to true in the defaults + multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple), + // same for debug + debugIsSet = args[0] && typeof args[0].debug !== 'undefined', + debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug); + + // initialize a tooltipster instance for each element if it doesn't already have one or if the multiple option is set, and attach the object to it + this.each(function () { + + var go = false, + ns = $(this).data('tooltipster-ns'), + instance = null; + + if (!ns) { + go = true; + } + else if (multiple) { + go = true; + } + else if (debug) { + console.log('Tooltipster: one or more tooltips are already attached to this element: ignoring. Use the "multiple" option to attach more tooltips.'); + } + + if (go) { + instance = new Plugin(this, args[0]); + + // save the reference of the new instance + if (!ns) ns = []; + ns.push(instance.namespace); + $(this).data('tooltipster-ns', ns) + + // save the instance itself + $(this).data(instance.namespace, instance); + } + + instances.push(instance); + }); + + if (multiple) return instances; + else return this; + } + } + }; + + // quick & dirty compare function (not bijective nor multidimensional) + function areEqual(a,b) { + var same = true; + $.each(a, function(i, el){ + if(typeof b[i] === 'undefined' || a[i] !== b[i]){ + same = false; + return false; + } + }); + return same; + } + + // detect if this device can trigger touch events + var deviceHasTouchCapability = !!('ontouchstart' in window); + + // we'll assume the device has no mouse until we detect any mouse movement + var deviceHasMouse = false; + $('body').one('mousemove', function() { + deviceHasMouse = true; + }); + + function deviceIsPureTouch() { + return (!deviceHasMouse && deviceHasTouchCapability); + } + + // detecting support for CSS transitions + function supportsTransitions() { + var b = document.body || document.documentElement, + s = b.style, + p = 'transition'; + + if(typeof s[p] == 'string') {return true; } + + v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'], + p = p.charAt(0).toUpperCase() + p.substr(1); + for(var i=0; i + +## Overview + +Sacred WordPress is a plugin for WordPress that searches the text of +entries and comments for Scripture references (e.g. John 3:16), tags +them with appropriate HTML markup, and on page load, retrieves the +Scripture text of each reference from a Bible API and displays it in +a tooltip when the user hovers over the reference. + +It will recognize references in a number of different formats: + +Format | Description +-----------------------|------------------------------------------ +John 6:53 | A simple, fully spelled-out Scripture reference +1 Corinthians 13:1 | A book with a numeric qualifier +First Corinthians 13:1 | A book with a numeric qualifier spelled out +1 Pet 3:21 | An abbreviated book name +Gen. 1:1 | Abbreviated with a period +Titus 1.3 | Using a period instead of a colon to identify chapter and verse +Romans 5:9-12 | A range of verses within a chapter +Romans 5:9–10 | A range using an en dash instead of a hyphen, as per proper style +Revelation 11:19–12:6 | A range of verses across multiple chapters +John 3:3,5 | A set of non-consecutive verses within a chapter +Jude 3 | A verse reference within a single-chapter book +Sirach 3:30 | A deuterocanonical reference +v. 10 or vv. 9–13 | Verse references separated from their book name; presumes the last referenced book and chapter + +For a demonstration, please see my own blog: [The Lonely Pilgrim] (http://lonelypilgrim.com/) + +This plugin is intentionally ecumenical, designed to be useful to +Christians of all sects and stripes. It supports the full range of +Scriptures, including the deuterocanon (known to Protestants as the +"apocrypha"). I've made every effort to include every common name and +abbreviation that the books of the Bible are known by in English. +If you know of anything I can or should add, please let me know. +I plan in the near future to add international support. + +If you find Sacred WordPress useful, [please let me know] (mailto:joseph.t.richardson@gmail.com)! + +## Requirements + +Sacred WordPress ought to work with any recent version of WordPress (v2.2+), +and requires jQuery 1.7+ (already built into WordPress) and +Tooltipster 3.0+ (packaged with Sacred WordPress). So all you really +need is a working WordPress installation. + +[That is, an installation of the WordPress.org blog platform, hosted on + your own server or shared hosting account. Sorry, but this doesn't + work with blogs hosted on WordPress.com.] + +You also need, as of the current version, a free API key to the +Bibles.org (American Bible Society Bible Search) API. I am planning, for +the first official release, support for other free APIs that don't +require getting an API key. But [getting a Bibles.org API key] (http://bibles.org/pages/api/signup) +is free and easy. + +## Installation + +To install Sacred WordPress, just drop it in your WordPress's +`/wp-content/plugins` directory, and enable it on the plugins page +of your Dashboard (`/wp-admin/plugins.php`). + +If you're using the Bibles.org API, you also need to edit +`bibles-org-request.php` in this package and put your own API key +into the variable `$APIKEY`. + +## Configuration + +I plan to give you a lot more options to configure, but as of the +current version, the most useful option is setting what Bible version +the tooltip will use. To do that, edit scripture.php in this package, +and in the `$config` array, change the values for `standard_version` +(for any Scripture reference that is not to the deuterocanonical books, +called by Protestants the "apocrypha") and `deutero_version` (for +deuterocanonical references). `standard_version` defaults to the ESV +(English Standard Version) and `deutero_version` defaults to KJVA +(King James Version with Apocrypha). Currently, the Bibles.org API +only supports a few English versions: + +Code | Version +----------|------------- +eng-AMP | Amplified Bible +eng-CEV | Contemporary English Version +eng-CEVD | Contemporary English Version (with deuterocanon) +eng-CEVUK | Contemporary English Version (Anglicised) +eng-ESV | English Standard Version +eng-GNTD | Good News Translation (with deuterocanon) (formerly known as Today's English Version) +eng-GNBDC | Good News Bible (with deuterocanon) (only apparent difference from GNTD is Anglicisation) +eng-KJV | King James Version +eng-KJVA | King James Version with Apocrypha +eng-MSG | The Message +eng-NASB | New American Standard Bible + +In `scriptureTooltip.js`, there are a couple of options you can set +(there will be more): + +Option | Description +------------------------------|----------------- +`defaultScriptureSite` | Sets the Bible website the Scripture header in the tooltip will link to. Defaults to BibleGateway.com. Not many more sites supported as yet, but it should be trivial to code support for another site. +`showCopyrightDataInTooltip` | If enabled, will place a 'Copyright Information' link in each Scripture toolip, giving basic copyright information on hover. I found it more aesthetic to disable this and include the copyright information in my blog footer. + +## Caveats + +At present, the plugin can be quite slow if called on a long page full +of full-text blog entries. On my blog, the front page can contain as +many as 90+ Scripture references, and it can take as long as 30 seconds +to load the Ajax response. I plan to implement some response caching to +improve on this problem. The response is much quicker when called on +individual entries. + +## To Do + +Some things I'd like to accomplish in future versions of Sacred WordPress: + +* General: + * Implement a mechanism for easy configuration. + * Internationalization: biblebooks.php for other-language users. + (Spanish, Italian, French, Portuguese, German, Dutch, + Russian, Greek, Turkish, Indonesian -- + probably in that order. Any others by request.) + +* In `scriptureTooltip.js` (the Ajax-requesting tooltip plugin): + * Adapt for optional use with jQuery UI tooltips. + * Implement link menu to various Bible sites in tooltips -- + will probably be implemented as a secondary popup. + * Allow an option to request Scripture texts separately on demand + rather than all as a lump at page load. + +* In `scripture.php`, etc. (the Ajax-responding, API-calling back end): + * Implement at least two other APIs: + * ESV.org + * Biblia.org + * (I am also planning my own API for Douay-Rheims requests.) + * Implement caching of Scripture text received from APIs. + * Adapt for optional use without Ajax -- + i.e. make calls for Scripture text during WordPress page load + and include static Scripture text hidden in WordPress output. + Would be most useful in combination with response caching. + +* In `tag_scripture.php` (the Scripture-tagging WordPress back end): + * Have it pass over references that are already tagged 'scriptureRef' + i.e. so we can manually tag problematic references without risk + of them being double-tagged (in which case we would get + double tooltips). + * At present there is no conflict with tagging references that are + already linked (i.e. `..`), since tag_scripture.php + applies `` tags and only styles them as links. + (This allows mobile users to click them to get tooltips without + actually following a link.) We can possibly refine this behavior. + +## License + +Sacred WordPress is released under the [MIT License] (http://opensource.org/licenses/MIT). +This means you are free to use, copy, edit, modify, reuse, redistribute, +incorporate all or part into your own project, or do pretty much anything +else you'd like with this code and software, provided you keep intact the +attribution to me and this license information. + +## References + +* [WordPress] (http://wordpress.org/) +* [WordPress Codex] (http://codex.wordpress.org/) +* [WordPress Code Reference] (https://developer.wordpress.org/reference/) +* [PHP Manual] (http://php.net/manual/en/) +* [jQuery] (http://www.jquery.com/) +* [jQuery API Documentation] (http://api.jquery.com/) +* [jQuery Learning Center] (http://learn.jquery.com/) +* [Tooltipster] (http://iamceege.github.io/tooltipster/) +* [Bibles.org API] (http://bibles.org/pages/api/) diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..f49a4ed --- /dev/null +++ b/README.txt @@ -0,0 +1,189 @@ +Sacred WordPress: A Scripture reference and tooltip plugin for WordPress. +http://jtrichardson.com/projects/sacred-wordpress +Version 0.10.1 +Copyright (c) 2014, Joseph T. Richardson + +OVERVIEW + +Sacred WordPress is a plugin for WordPress that searches the text of +entries and comments for Scripture references (e.g. John 3:16), tags +them with appropriate HTML markup, and on page load, retrieves the +Scripture text of each reference from a Bible API and displays it in +a tooltip when the user hovers over the reference. + +It will recognize references in a number of different formats: + + John 6:53 A simple, fully spelled-out Scripture reference + 1 Corinthians 13:1 A book with a numeric qualifier + First Corinthians 13:1 A book with a numeric qualifier spelled out + 1 Pet 3:21 An abbreviated book name + Gen. 1:1 Abbreviated with a period + Titus 1.3 Using a period instead of a colon to identify + chapter and verse + Romans 5:9-12 A range of verses within a chapter + Romans 5:9–10 A range using an en dash instead of a hyphen, + as per proper style + Revelation 11:19–12:6 A range of verses across multiple chapters + John 3:3,5 A set of non-consecutive verses within a chapter + Jude 3 A verse reference within a single-chapter book + Sirach 3:30 A deuterocanonical reference + v. 10 or vv. 9–13 Verse references separated from their book name; + presumes the last referenced book and chapter + +For a demonstration, please see my own blog: + + The Lonely Pilgrim http://lonelypilgrim.com/ + +This plugin is intentionally ecumenical, designed to be useful to +Christians of all sects and stripes. It supports the full range of +Scriptures, including the deuterocanon (known to Protestants as the +"apocrypha"). I've made every effort to include every common name and +abbreviation that the books of the Bible are known by in English. +If you know of anything I can or should add, please let me know. +I plan in the near future to add international support. + +If you find Sacred WordPress useful, please let me know! + +REQUIREMENTS + +Sacred WordPress ought to work with any recent version of WordPress (v2.2+), +and requires jQuery 1.7+ (already built into WordPress) and +Tooltipster 3.0+ (packaged with Sacred WordPress). So all you really +need is a working WordPress installation. + +* That is, an installation of the WordPress.org blog platform, hosted on + your own server or shared hosting account. Sorry, but this doesn't + work with blogs hosted on WordPress.com. + +You also need, as of the current version, a free API key to the +Bibles.org (American Bible Society Bible Search) API. I am planning, for +the first official release, support for other free APIs that don't +require getting an API key. But getting a Bibles.org API key is +free and easy. See http://bibles.org/pages/api/signup. + +INSTALLATION + +To install Sacred WordPress, just drop it in your WordPress's +/wp-content/plugins directory, and enable it on the plugins page +of your Dashboard (/wp-admin/plugins.php). + +If you're using the Bibles.org API, you also need to edit +bibles-org-request.php in this package and put your own API key +into the variable $APIKEY. + +CONFIGURATION + +I plan to give you a lot more options to configure, but as of the +current version, the most useful option is setting what Bible version +the tooltip will use. To do that, edit scripture.php in this package, +and in the $config array, change the values for 'standard_version' +(for any Scripture reference that is not to the deuterocanonical books, +called by Protestants the "apocrypha") and 'deutero_version' (for +deuterocanonical references). 'standard_version' defaults to the ESV +(English Standard Version) and 'deutero_version' defaults to KJVA +(King James Version with Apocrypha). Currently, the Bibles.org API +only supports a few English versions: + + eng-AMP Amplified Bible + eng-CEV Contemporary English Version + eng-CEVD Contemporary English Version (with deuterocanon) + eng-CEVUK Contemporary English Version (Anglicised) + eng-ESV English Standard Version + eng-GNTD Good News Translation (with deuterocanon) + (formerly known as Today's English Version) + eng-GNBDC Good News Bible (with deuterocanon) + (only apparent difference from GNTD is Anglicisation) + eng-KJV King James Version + eng-KJVA King James Version with Apocrypha + eng-MSG The Message + eng-NASB New American Standard Bible + +In scriptureTooltip.js, there are a couple of options you can set +(there will be more): + + defaultScriptureSite: Sets the Bible website the Scripture header + in the tooltip will link to. Defaults + to BibleGateway.com. Not many more sites + supported as yet, but it should be + trivial to code support for another site. + + showCopyrightDataInTooltip: If enabled, will place a + 'Copyright Information' link in + each Scripture toolip, giving basic + copyright information on hover. + I found it more aesthetic to disable + this and include the copyright + information in my blog footer. + +CAVEATS + +At present, the plugin can be quite slow if called on a long page full +of full-text blog entries. On my blog, the front page can contain as +many as 90+ Scripture references, and it can take as long as 30 seconds +to load the Ajax response. I plan to implement some response caching to +improve on this problem. The response is much quicker when called on +individual entries. + +TO DO + +Some things I'd like to accomplish in future versions of Sacred WordPress: + + General: + * Implement a mechanism for easy configuration. + * Internationalization: biblebooks.php for other-language users. + (Spanish, Italian, French, Portuguese, German, Dutch, + Russian, Greek, Turkish, Indonesian -- + probably in that order. Any others by request.) + + In scriptureTooltip.js (the Ajax-requesting tooltip plugin): + * Adapt for optional use with jQuery UI tooltips. + * Implement link menu to various Bible sites in tooltips -- + will probably be implemented as a secondary popup. + * Allow an option to request Scripture texts separately on demand + rather than all as a lump at page load. + + In scripture.php, etc. (the Ajax-responding, API-calling back end): + * Implement at least two other APIs: + ESV.org + Biblia.org + (I am also planning my own API for Douay-Rheims requests.) + * Implement caching of Scripture text received from APIs. + * Adapt for optional use without Ajax -- + i.e. make calls for Scripture text during WordPress page load + and include static Scripture text hidden in WordPress output. + Would be most useful in combination with response caching. + + In tag_scripture.php (the Scripture-tagging WordPress back end): + * Have it pass over references that are already tagged 'scriptureRef' + i.e. so we can manually tag problematic references without risk + of them being double-tagged (in which case we would get + double tooltips). + * At present there is no conflict with tagging references that are + already linked (i.e. ..), since tag_scripture.php + applies tags and only styles them as links. + (This allows mobile users to click them to get tooltips without + actually following a link.) We can possibly refine this behavior. + +LICENSE + +Sacred WordPress is released under the MIT License (http://opensource.org/licenses/MIT). +This means you are free to use, copy, edit, modify, reuse, redistribute, +incorporate all or part into your own project, or do pretty much anything +else you'd like with this code and software, provided you keep intact the +attribution to me and this license information. + +REFERENCES + + WordPress http://wordpress.org/ + WordPress Codex http://codex.wordpress.org/ + WordPress Code Reference https://developer.wordpress.org/reference/ + + PHP Manual http://php.net/manual/en/ + + jQuery http://www.jquery.com/ + jQuery API Documentation http://api.jquery.com/ + jQuery Learning Center http://learn.jquery.com/ + + Tooltipster http://iamceege.github.io/tooltipster/ + + Bibles.org API http://bibles.org/pages/api/ diff --git a/biblebooks.php b/biblebooks.php new file mode 100644 index 0000000..d204e23 --- /dev/null +++ b/biblebooks.php @@ -0,0 +1,493 @@ + ['Gen', 'Gn', 'Ge'], + 'Exodus' => ['Exod', 'Ex', 'Exo'], + 'Leviticus' => ['Lev', 'Lv', 'Le'], + 'Numbers' => ['Num', 'Nm', 'Nu', 'Nb'], + 'Deuteronomy' => ['Deut', 'Dt'], + 'Joshua' => ['Josh', 'Jo', 'Jos'], + 'Judges' => ['Judg', 'Jgs', 'Jdg', 'Jg'], + 'Ruth' => ['Ru', 'Rth'], + '1 Samuel' => ['1 Sam', '1 Sm', '1 Sa'], + '2 Samuel' => ['2 Sam', '2 Sm', '2 Sa'], + '1 Kings' => ['1 Kgs', '1 Ki'], + '2 Kings' => ['2 Kgs', '2 Ki'], + '1 Chronicles' => ['1 Chron', '1 Chr', '1 Ch', '1 Par'], + '2 Chronicles' => ['2 Chron', '2 Chr', '2 Ch', '2 Par'], + 'Ezra' => ['Ezr'], + 'Nehemiah' => ['Neh', 'Ne'], + 'Tobit' => ['Tob', 'Tb'], + 'Judith' => ['Jdt'], + 'Esther' => ['Es', 'Est', 'Esth'], + '1 Maccabees' => ['1 Macc', '1 Mc'], + '2 Maccabees' => ['2 Macc', '2 Mc'], + 'Job' => ['Job', 'Jb'], + 'Psalms' => ['Ps', 'Pss', 'Psalm', 'Psa'], + 'Proverbs' => ['Prov', 'Prv', 'Pr'], + 'Ecclesiastes' => ['Eccles', 'Eccl', 'Ec', 'Qoh'], + 'Song of Songs' => ['Song of Sol', 'Song', 'Sg', 'Cant', 'Canticle', 'Canticles'], + 'Wisdom' => ['Ws', 'Wis', 'Wisd', 'Wisd of Sol'], + 'Sirach' => ['Sir', 'Ecclus'], + 'Isaiah' => ['Isa', 'Is'], + 'Jeremiah' => ['Jer', 'Je'], + 'Lamentations' => ['Lam', 'La'], + 'Baruch' => ['Bar'], + 'Ezekiel' => ['Ezek', 'Ez', 'Eze', 'Ezk'], + 'Daniel' => ['Dan', 'Dn', 'Da'], + 'Hosea' => ['Hos', 'Os', 'Ho'], + 'Joel' => ['Joel', 'Jl', 'Joe'], + 'Amos' => ['Am'], + 'Obadiah' => ['Obad', 'Ob', 'Abd'], + 'Jonah' => ['Jon'], + 'Micah' => ['Mic', 'Mi'], + 'Nahum' => ['Nah', 'Na'], + 'Habakkuk' => ['Hab', 'Hb'], + 'Zephaniah' => ['Zeph', 'Zep', 'Soph'], + 'Haggai' => ['Hag', 'Hg', 'Agg'], + 'Zechariah' => ['Zech', 'Zec', 'Zac', 'Zach'], + 'Malachi' => ['Mal'], + 'Matthew' => ['Matt', 'Mt'], + 'Mark' => ['Mk'], + 'Luke' => ['Lk', 'Luk'], + 'John' => ['Jn', 'Jhn'], + 'Acts' => ['Ac'], + 'Romans' => ['Rom', 'Ro'], + '1 Corinthians' => ['1 Cor', '1 Co'], + '2 Corinthians' => ['2 Cor', '2 Co'], + 'Galatians' => ['Gal', 'Ga'], + 'Ephesians' => ['Eph', 'Ephes'], + 'Philippians' => ['Phil', 'Php'], + 'Colossians' => ['Col'], + '1 Thessalonians' => ['1 Thess', '1 Thes', '1 Th'], + '2 Thessalonians' => ['2 Thess', '2 Thes', '2 Th'], + '1 Timothy' => ['1 Tim', '1 Tm', '1 Ti'], + '2 Timothy' => ['2 Tim', '2 Tm', '2 Ti'], + 'Titus' => ['Ti', 'Tit'], + 'Philemon' => ['Philem', 'Phlm', 'Phm'], + 'Hebrews' => ['Heb'], + 'James' => ['Jas', 'Jm'], + '1 Peter' => ['1 Pet', '1 Pt', '1 Pe'], + '2 Peter' => ['2 Pet', '2 Pt', '2 Pe'], + '1 John' => ['1 Jn', '1 Jo'], + '2 John' => ['2 Jn', '2 Jo'], + '3 John' => ['3 Jn', '3 Jo'], + 'Revelation' => ['Rev', 'Rv', 'Re', 'Apoc'], +); + +$BIBLEBOOKS['ALT_NAMES'] = array( + 'Song of Solomon' => 'Song of Songs', + 'Apocalypse' => 'Revelation', + 'Josue' => 'Joshua', + 'Iesous' => 'Joshua', + '3 Kings' => '1 Kings', + '4 Kings' => '2 Kings', + '3 Kingdoms' => '1 Kings', + '4 Kingdoms' => '2 Kings', + '1 Paralipomenon' => '1 Chronicles', + '2 Paralipomenon' => '2 Chronicles', + #'1 Esdras' => 'Ezra', # In Latin Vulgate these referred to + #'2 Esdras' => 'Nehemiah', # Ezr and Neh, but this is no longer common + 'Nehemias' => 'Nehemiah', + 'Tobias' => 'Tobit', + 'Qoholeth' => 'Ecclesiastes', + 'Canticle of Canticles' => 'Song of Songs', + 'Ecclesiasticus' => 'Sirach', + 'Isaias' => 'Isaiah', + 'Jeremias' => 'Jeremiah', + 'Ezechiel' => 'Ezekiel', + 'Osee' => 'Hosea', + 'Abdias' => 'Obadiah', + 'Jonas' => 'Jonah', + 'Micheas' => 'Micah', + 'Habacuc' => 'Habakkuk', + 'Sophonias' => 'Zephaniah', + 'Aggeus' => 'Haggai', + 'Zacharias' => 'Zechariah', + 'Zachariah' => 'Zechariah', + 'Malachias' => 'Malachi', + '1 Machabees' => '1 Maccabees', + '2 Machabees' => '2 Maccabees', + 'Acts of the Apostles' => 'Acts', +); + +/* These are the standard abbreviations that, as of 2014/08/24, the Bibles.org API uses. + * They aren't currently being used for anything here. */ +/* $BIBLEBOOKS['CANONICAL_ABBREVS'] = array( + 'Genesis' => 'Gen', + 'Exodus' => 'Exod', + 'Leviticus' => 'Lev', + 'Numbers' => 'Num', + 'Deuteronomy' => 'Deut', + 'Joshua' => 'Josh', + 'Judges' => 'Judg', + 'Ruth' => 'Ruth', + '1 Samuel' => '1Sam', + '2 Samuel' => '2Sam', + '1 Kings' => '1Kgs', + '2 Kings' => '2Kgs', + '1 Chronicles' => '1Chr', + '2 Chronicles' => '2Chr', + 'Ezra' => 'Ezra', + 'Nehemiah' => 'Neh', + 'Tobit' => 'Tob', + 'Judith' => 'Jdt', + 'Esther' => 'Esth', + '1 Maccabees' => '1Macc', + '2 Maccabees' => '2Macc', + 'Job' => 'Job', + 'Psalms' => 'Ps', + 'Proverbs' => 'Prov', + 'Ecclesiastes' => 'Eccl', + 'Song of Songs' => 'Song', + 'Wisdom' => 'Wis', + 'Sirach' => 'Sir', + 'Isaiah' => 'Isa', + 'Jeremiah' => 'Jer', + 'Lamentations' => 'Lam', + 'Baruch' => 'Bar', + 'Ezekiel' => 'Ezek', + 'Daniel' => 'Dan', + 'Hosea' => 'Hos', + 'Joel' => 'Joel', + 'Amos' => 'Amos', + 'Obadiah' => 'Obad', + 'Jonah' => 'Jonah', + 'Micah' => 'Mic', + 'Nahum' => 'Nah', + 'Habakkuk' => 'Hab', + 'Zephaniah' => 'Zeph', + 'Haggai' => 'Hag', + 'Zechariah' => 'Zech', + 'Malachi' => 'Mal', + 'Matthew' => 'Matt', + 'Mark' => 'Mark', + 'Luke' => 'Luke', + 'John' => 'John', + 'Acts' => 'Acts', + 'Romans' => 'Rom', + '1 Corinthians' => '1Cor', + '2 Corinthians' => '2Cor', + 'Galatians' => 'Gal', + 'Ephesians' => 'Eph', + 'Philippians' => 'Phil', + 'Colossians' => 'Col', + '1 Thessalonians' => '1Thess', + '2 Thessalonians' => '2Thess', + '1 Timothy' => '1Tim', + '2 Timothy' => '2Tim', + 'Titus' => 'Titus', + 'Philemon' => 'Phlm', + 'Hebrews' => 'Heb', + 'James' => 'Jas', + '1 Peter' => '1Pet', + '2 Peter' => '2Pet', + '1 John' => '1John', + '2 John' => '2John', + '3 John' => '3John', + 'Jude' => 'Jude', + 'Revelation' => 'Rev', +); */ + +/** + * How many chapters are in each biblical book, for use, first of all, + * in determining which books are a single chapter. I also thought it + * could be useful in some cases (possibly along with [CHAPTER_VERSES] + * below) in resolving ambiguous references between different books which + * sometimes share the same name -- e.g. By "1 Kings 25:1," did the + * reference actually mean 1 Kings, or 1 Samuel (called 1 Kings in the + * Vulgate and other translations proceeding from the Septuagint, the + * books called 1 and 2 Kings in modern editions being called 3 and 4 Kings). + * It happens that the modern 1 Kings has only 22 chapters, therefore + * 1 Kings 25:1 probably refers to 1 Samuel 25:1. + */ +$BIBLEBOOKS['CHAPTERS'] = array( + 'Genesis' => 50, + 'Exodus' => 40, + 'Leviticus' => 27, + 'Numbers' => 36, + 'Deuteronomy' => 34, + 'Joshua' => 24, + 'Judges' => 21, + 'Ruth' => 4, + '1 Samuel' => 31, + '2 Samuel' => 24, + '1 Kings' => 22, + '2 Kings' => 25, + '1 Chronicles' => 29, + '2 Chronicles' => 36, + 'Ezra' => 10, + 'Nehemiah' => 13, + 'Tobit' => 14, + 'Judith' => 16, + 'Esther' => 10, # 10 in Greek + '1 Maccabees' => 16, + '2 Maccabees' => 15, + 'Job' => 42, + 'Psalms' => 150, + 'Proverbs' => 31, + 'Ecclesiastes' => 12, + 'Song of Songs' => 8, + 'Wisdom' => 19, + 'Sirach' => 51, + 'Isaiah' => 66, + 'Jeremiah' => 52, + 'Lamentations' => 5, + 'Baruch' => 5, # Letter of Jeremiah is Baruch 6 in Catholic Bibles + 'Ezekiel' => 48, + 'Daniel' => 12, + 'Hosea' => 14, + 'Joel' => 3, + 'Amos' => 9, + 'Obadiah' => 1, + 'Jonah' => 4, + 'Micah' => 7, + 'Nahum' => 3, + 'Habakkuk' => 3, + 'Zephaniah' => 3, + 'Haggai' => 2, + 'Zechariah' => 14, + 'Malachi' => 4, + '1 Esdras' => 9, # Septuagint 1 Esdras, Vulgate 3 Esdras + '2 Esdras' => 16, # Vulgate 4 Esdras + 'Prayer of Manasseh' => 1, + 'Matthew' => 28, + 'Mark' => 16, + 'Luke' => 24, + 'John' => 21, + 'Acts' => 28, + 'Romans' => 16, + '1 Corinthians' => 16, + '2 Corinthians' => 13, + 'Galatians' => 6, + 'Ephesians' => 6, + 'Philippians' => 4, + 'Colossians' => 4, + '1 Thessalonians' => 5, + '2 Thessalonians' => 3, + '1 Timothy' => 6, + '2 Timothy' => 4, + 'Titus' => 3, + 'Philemon' => 1, + 'Hebrews' => 13, + 'James' => 5, + '1 Peter' => 5, + '2 Peter' => 3, + '1 John' => 5, + '2 John' => 1, + '3 John' => 1, + 'Jude' => 1, + 'Revelation' => 22, +); + +/** + * The number of verses in each chapter of each book, according to the ESV. + * This seems to be fairly consistent across modern versions. This can used, + * like [CHAPTERS], to resolve ambiguous references. + * Also it is used to guess how many verses we are looking for when the + * user requests a whole chapter, since the Bibles.org API returns whole + * chapter requests (e.g. "1 Corinthians 13") in the form of + * "1 Corinthians 13:1-13". + */ +$BIBLEBOOKS['CHAPTER_VERSES'] = array( + 'Genesis' => [31, 25, 24, 26, 32, 22, 24, 22, 29, 32, 32, 20, 18, 24, 21, 16, 27, 33, 38, 18, 34, 24, 20, 67, 34, 35, 46, 22, 35, 43, 55, 32, 20, 31, 29, 43, 36, 30, 23, 23, 57, 38, 34, 34, 28, 34, 31, 22, 33, 26], + 'Exodus' => [22, 25, 22, 31, 23, 30, 25, 32, 35, 29, 10, 51, 22, 31, 27, 36, 16, 27, 25, 26, 36, 31, 33, 18, 40, 37, 21, 43, 46, 38, 18, 35, 23, 35, 35, 38, 29, 31, 43, 38], + 'Leviticus' => [17, 16, 17, 35, 19, 30, 38, 36, 24, 20, 47, 8, 59, 57, 33, 34, 16, 30, 37, 27, 24, 33, 44, 23, 55, 46, 34], + 'Numbers' => [54, 34, 51, 49, 31, 27, 89, 26, 23, 36, 35, 16, 33, 45, 41, 50, 13, 32, 22, 29, 35, 41, 30, 25, 18, 65, 23, 31, 40, 16, 54, 42, 56, 29, 34, 13], + 'Deuteronomy' => [46, 37, 29, 49, 33, 25, 26, 20, 29, 22, 32, 32, 18, 29, 23, 22, 20, 22, 21, 20, 23, 30, 25, 22, 19, 19, 26, 68, 29, 20, 30, 52, 29, 12], + 'Joshua' => [18, 24, 17, 24, 15, 27, 26, 35, 27, 43, 23, 24, 33, 15, 63, 10, 18, 28, 51, 9, 45, 34, 16, 33], + 'Judges' => [36, 23, 31, 24, 31, 40, 25, 35, 57, 18, 40, 15, 25, 20, 20, 31, 13, 31, 30, 48, 25], + 'Ruth' => [22, 23, 18, 22], + '1 Samuel' => [28, 36, 21, 22, 12, 21, 17, 22, 27, 27, 15, 25, 23, 52, 35, 23, 58, 30, 24, 42, 15, 23, 29, 22, 44, 25, 12, 25, 11, 31, 13], + '2 Samuel' => [27, 32, 39, 12, 25, 23, 29, 18, 13, 19, 27, 31, 39, 33, 37, 23, 29, 33, 43, 26, 22, 51, 39, 25], + '1 Kings' => [53, 46, 28, 34, 18, 38, 51, 66, 28, 29, 43, 33, 34, 31, 34, 34, 24, 46, 21, 43, 29, 53], + '2 Kings' => [18, 25, 27, 44, 27, 33, 20, 29, 37, 36, 21, 21, 25, 29, 38, 20, 41, 37, 37, 21, 26, 20, 37, 20, 30], + '1 Chronicles' => [54, 55, 24, 43, 26, 81, 40, 40, 44, 14, 47, 40, 14, 17, 29, 43, 27, 17, 19, 8, 30, 19, 32, 31, 31, 32, 34, 21, 30], + '2 Chronicles' => [17, 18, 17, 22, 14, 42, 22, 18, 31, 19, 23, 16, 22, 15, 19, 14, 19, 34, 11, 37, 20, 12, 21, 27, 28, 23, 9, 27, 36, 27, 21, 33, 25, 33, 27, 23], + 'Ezra' => [11, 70, 13, 24, 17, 22, 28, 36, 15, 44], + 'Nehemiah' => [11, 20, 32, 23, 19, 19, 73, 18, 38, 39, 36, 47, 31], + 'Tobit' => [22, 14, 17, 21, 22, 17, 18, 21, 6, 12, 19, 22, 18, 15], # from KJVA + 'Judith' => [16, 28, 10, 15, 24, 21, 32, 36, 14, 23, 23, 20, 20, 19, 13, 25], # from KJVA + 'Esther' => [22, 23, 15, 17, 14, 14, 10, 17, 32, 3], + '1 Maccabees' => [64, 70, 60, 61, 68, 63, 50, 32, 73, 89, 74, 53, 53, 49, 41, 24], # from KJVA + '2 Maccabees' => [36, 32, 40, 50, 27, 31, 42, 36, 29, 38, 38, 45, 26, 46, 39], # from KJVA + 'Job' => [22, 13, 26, 21, 27, 30, 21, 22, 35, 22, 20, 25, 28, 22, 35, 22, 16, 21, 29, 29, 34, 30, 17, 25, 6, 14, 23, 28, 25, 31, 40, 22, 33, 37, 16, 33, 24, 41, 30, 24, 34, 17], + 'Psalms' => [6, 12, 8, 8, 12, 10, 17, 9, 20, 18, 7, 8, 6, 7, 5, 11, 15, 50, 14, 9, 13, 31, 6, 10, 22, 12, 14, 9, 11, 12, 24, 11, 22, 22, 28, 12, 40, 22, 13, 17, 13, 11, 5, 26, 17, 11, 9, 14, 20, 23, 19, 9, 6, 7, 23, 13, 11, 11, 17, 12, 8, 12, 11, 10, 13, 20, 7, 35, 36, 5, 24, 20, 28, 23, 10, 12, 20, 72, 13, 19, 16, 8, 18, 12, 13, 17, 7, 18, 52, 17, 16, 15, 5, 23, 11, 13, 12, 9, 9, 5, 8, 28, 22, 35, 45, 48, 43, 13, 31, 7, 10, 10, 9, 8, 18, 19, 2, 29, 176, 7, 8, 9, 4, 8, 5, 6, 5, 6, 8, 8, 3, 18, 3, 3, 21, 26, 9, 8, 24, 13, 10, 7, 12, 15, 21, 10, 20, 14, 9, 6], + 'Proverbs' => [33, 22, 35, 27, 23, 35, 27, 36, 18, 32, 31, 28, 25, 35, 33, 33, 28, 24, 29, 30, 31, 29, 35, 34, 28, 28, 27, 28, 27, 33, 31], + 'Ecclesiastes' => [18, 26, 22, 16, 20, 12, 29, 17, 18, 20, 10, 14], + 'Song of Songs' => [17, 17, 11, 16, 16, 13, 13, 14], + 'Wisdom' => [16, 24, 19, 20, 23, 25, 30, 21, 18, 21, 26, 27, 19, 31, 19, 29, 21, 25, 22], # from KJVA + 'Sirach' => [30, 18, 31, 31, 15, 37, 36, 19, 18, 31, 34, 18, 26, 27, 20, 30, 32, 33, 30, 32, 28, 27, 28, 34, 26, 29, 30, 26, 28, 25, 31, 24, 31, 26, 20, 26, 31, 34, 35, 30, 24, 25, 33, 23, 26, 20, 25, 25, 16, 29, 30], # from KJVA + 'Isaiah' => [31, 22, 26, 6, 30, 13, 25, 22, 21, 34, 16, 6, 22, 32, 9, 14, 14, 7, 25, 6, 17, 25, 18, 23, 12, 21, 13, 29, 24, 33, 9, 20, 24, 17, 10, 22, 38, 22, 8, 31, 29, 25, 28, 28, 25, 13, 15, 22, 26, 11, 23, 15, 12, 17, 13, 12, 21, 14, 21, 22, 11, 12, 19, 12, 25, 24], + 'Jeremiah' => [19, 37, 25, 31, 31, 30, 34, 22, 26, 25, 23, 17, 27, 22, 21, 21, 27, 23, 15, 18, 14, 30, 40, 10, 38, 24, 22, 17, 32, 24, 40, 44, 26, 22, 19, 32, 21, 28, 18, 16, 18, 22, 13, 30, 5, 28, 7, 47, 39, 46, 64, 34], + 'Lamentations' => [22, 22, 66, 22, 22], + 'Baruch' => [22, 35, 37, 37, 9, 73], # from KJVA + 'Ezekiel' => [28, 10, 27, 17, 17, 14, 27, 18, 11, 22, 25, 28, 23, 23, 8, 63, 24, 32, 14, 49, 32, 31, 49, 27, 17, 21, 36, 26, 21, 26, 18, 32, 33, 31, 15, 38, 28, 23, 29, 49, 26, 20, 27, 31, 25, 24, 23, 35], + 'Daniel' => [21, 49, 30, 37, 31, 28, 28, 27, 27, 21, 45, 13], + 'Hosea' => [11, 23, 5, 19, 15, 11, 16, 14, 17, 15, 12, 14, 16, 9], + 'Joel' => [20, 32, 21], + 'Amos' => [15, 16, 15, 13, 27, 14, 17, 14, 15], + 'Obadiah' => [21], + 'Jonah' => [17, 10, 10, 11], + 'Micah' => [16, 13, 12, 13, 15, 16, 20], + 'Nahum' => [15, 13, 19], + 'Habakkuk' => [17, 20, 19], + 'Zephaniah' => [18, 15, 20], + 'Haggai' => [15, 23], + 'Zechariah' => [21, 13, 10, 14, 11, 15, 14, 23, 17, 12, 17, 14, 9, 21], + 'Malachi' => [14, 17, 18, 6], + 'Matthew' => [25, 23, 17, 25, 48, 34, 29, 34, 38, 42, 30, 50, 58, 36, 39, 28, 27, 35, 30, 34, 46, 46, 39, 51, 46, 75, 66, 20], + 'Mark' => [45, 28, 35, 41, 43, 56, 37, 38, 50, 52, 33, 44, 37, 72, 47, 20], + 'Luke' => [80, 52, 38, 44, 39, 49, 50, 56, 62, 42, 54, 59, 35, 35, 32, 31, 37, 43, 48, 47, 38, 71, 56, 53], + 'John' => [51, 25, 36, 54, 47, 71, 53, 59, 41, 42, 57, 50, 38, 31, 27, 33, 26, 40, 42, 31, 25], + 'Acts' => [26, 47, 26, 37, 42, 15, 60, 40, 43, 48, 30, 25, 52, 28, 41, 40, 34, 28, 41, 38, 40, 30, 35, 27, 27, 32, 44, 31], + 'Romans' => [32, 29, 31, 25, 21, 23, 25, 39, 33, 21, 36, 21, 14, 23, 33, 27], + '1 Corinthians' => [31, 16, 23, 21, 13, 20, 40, 13, 27, 33, 34, 31, 13, 40, 58, 24], + '2 Corinthians' => [24, 17, 18, 18, 21, 18, 16, 24, 15, 18, 33, 21, 14], + 'Galatians' => [24, 21, 29, 31, 26, 18], + 'Ephesians' => [23, 22, 21, 32, 33, 24], + 'Philippians' => [30, 30, 21, 23], + 'Colossians' => [29, 23, 25, 18], + '1 Thessalonians' => [10, 20, 13, 18, 28], + '2 Thessalonians' => [12, 17, 18], + '1 Timothy' => [20, 15, 16, 16, 25, 21], + '2 Timothy' => [18, 26, 17, 22], + 'Titus' => [16, 15, 15], + 'Philemon' => [25], + 'Hebrews' => [14, 18, 19, 16, 14, 20, 28, 13, 28, 39, 40, 29, 25], + 'James' => [27, 26, 18, 17, 20], + '1 Peter' => [25, 25, 22, 19, 14], + '2 Peter' => [21, 22, 18], + '1 John' => [10, 29, 24, 21, 21], + '2 John' => [13], + '3 John' => [15], + 'Jude' => [25], + 'Revelation' => [20, 29, 22, 11, 14, 17, 17, 13, 21, 11, 19, 17, 18, 20, 8, 21, 18, 24, 21, 15, 27, 21] +); + +/** + * What books are deuterocanonical (and therefore not in most Protestant + * editions of Scripture)? + */ +$BIBLEBOOKS['DEUTERO'] = array( + 'Tobit' => 1, + 'Judith' => 1, + '1 Maccabees' => 1, + '2 Maccabees' => 1, + 'Wisdom' => 1, + 'Sirach' => 1, + 'Baruch' => 1, + + '1 Esdras' => 1, + '2 Esdras' => 1, + 'Prayer of Manasseh' => 1, +); + +class BibleBooks { + + /** + * Functions to map this all to a large [BOOK] array, so that any + * permutation of a scriptural reference will match to something + * in the one array + */ + static private function map_books(array $books) { + global $BIBLEBOOKS; + foreach ($books as $book) { + $BIBLEBOOKS['BOOKS'][$book] = $book; + } + } + + static private function map_alt(array $books) { + global $BIBLEBOOKS; + foreach ($books as $alt => $book) { + $BIBLEBOOKS['BOOKS'][$alt] = $book; + } + } + + static private function map_abbrevs(array $all_abbrevs) { + global $BIBLEBOOKS; + foreach ($all_abbrevs as $book => $abbrevs) { + foreach ($abbrevs as $abbrev) { + $BIBLEBOOKS['BOOKS'][$abbrev] = $book; + } + } + } + + /** + * Function to trigger all of the above. + */ + static public function prepare_book_maps() { + global $BIBLEBOOKS; + $BIBLEBOOOKS['BOOKS'] = array(); + self::map_books($BIBLEBOOKS['BOOKS_OT']); + self::map_books($BIBLEBOOKS['BOOKS_NT']); + self::map_alt($BIBLEBOOKS['ALT_NAMES']); + self::map_abbrevs($BIBLEBOOKS['ABBREVS']); + } +} diff --git a/bibles-org-request.php b/bibles-org-request.php new file mode 100644 index 0000000..e0e6f28 --- /dev/null +++ b/bibles-org-request.php @@ -0,0 +1,219 @@ + "https://bibles.org/v2/passages.js" + ); + + /* API will include footnotes and crossreferences in text, but it + * is very muddy HTML and it was more information than I wanted to + * load in the tooltip. If you want it and want to deal with it, + * you can enable it here. + */ + private $include_marginalia = 0; + + /* This keeps the responses until we package them to return. */ + private $buffer; + private $copyright; + + /** + * We use Curl to make our requests to the API. + * Initialize it here. + */ + private function init_curl() { + $apikey = $this->APIKEY; + $ch = curl_init(); + // Don't verify SSL certificate. + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + // Return the response as a string. + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + // Follow any redirects. + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + // Set up authentication. + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($ch, CURLOPT_USERPWD, $apikey . ":X"); + return $ch; + } + + /** + * Actually make the request (a string of all passages we want) + * to the API via Curl. + * + * @param $url The REST URL containing the query string. + */ + private function curl_get($url) { + $ch = $this->init_curl(); + curl_setopt($ch, CURLOPT_URL, $url); + $response = curl_exec($ch); + curl_close($ch); + return $response; # As JSON + } + + /** + * This is the method called by the main ScriptureRequest object + * to make the request to the API. It retrieves the response and + * returns it in the format ScriptureRequest is expecting. + * + * @param $ref The string of references we want. + * @param $version The Bible version we want them in, in the form API expects. + * + * (See https://bibles.org/v2/versions.xml [for XML] or + * https://bibles.org/v2/versions.js [for JSON] with your API key + * [i.e. https://APIKEY:X@https://bibles.org:443/...] + * to get a list of all the supported versions, in a multiplicity + * of different languages. + * https://bibles.org/v2/versions.xml?language=eng-US and + * https://bibles.org/v2/versions.xml?language=eng-GB are easier + * to manage if you want English (of which, as of now, there are + * only a few supported versions: + * eng-AMP Amplified Bible + * eng-CEV Contemporary English Version + * eng-CEVD Contemporary English Version (with deuterocanon) + * eng-CEVUK Contemporary English Version (Anglicised) + * eng-ESV English Standard Version + * eng-GNTD Good News Translation (with deuterocanon) + * (formerly known as Today's English Version) + * eng-GNBDC Good News Bible (with deuterocanon) + * (only apparent difference from GNTD is + * Anglicisation) + * eng-KJV King James Version + * eng-KJVA King James Version with Apocrypha + * eng-MSG The Message + * eng-NASB New American Standard Bible + * Other useful highlihghts: + * por-NTLH Nova Tradução na Linguagem de Hoje (Portuguese) + * spa-DHH Biblia Dios Habla Hoy (Spanish) + * spa-RVR60 Biblia Reina Valera 1960 (Spanish) + * cym-BCN Beibl Cymraeg Newydd (Welsh) + */ + public function get_passages($ref, $version) { + # This will send query string of semicolon-separated references + # and receive a JSON object with members. + # (Comma-separated also works with this API.) + ScriptureRequest::debug_print("[bibles-org-request] get_passages('$ref')\n"); + + # This isn't really necessary I don't think, but play it safe. + $ref = preg_replace('/\s+/', '+', $ref); + + $url = $this->urls["passages"] . "?q[]=" . $ref; + $url .= "&version=" . $version; + if ($this->include_marginalia) { + $url .= "&include_marginalia=true"; + } + $jsonstr = $this->curl_get($url); + + $refs = explode(';', $ref); + $this->parse_passages_response($jsonstr, $refs); + + $wrapper = $this->return_data(); + return $wrapper; + } + + /** + * Parse the JSON received from the API, reformat it as we want it, + * and return it in a form we can use. + * + * @param $jsonstr The JSON string received from the API. + * @param $refs An array of the requests we made. + */ + public function parse_passages_response($jsonstr, $refs) { + $json = json_decode($jsonstr); + if (! $json) { + # Then something is wrong; we didn't receive any JSON. + return []; + } + $passages = $json->response->search->result->passages; + $fums_tid = $json->response->meta->fums_tid; + + # A container to keep all our copyright information. + $copyright_obj = array(); + + foreach ($passages as $passage) { + $return_text = ""; + $version = $passage->version_abbreviation; + + $copyright = $passage->copyright; + # Strip the API's HTML from copyright data; we are going + # to format it otherwise. + $copyright = preg_replace('/<\/?p>/', '', $copyright); + $copyright = trim($copyright); + # We only need the copyright info once per version. + # This saves a good many bytes in the response. + $copyright_obj[$version] = $copyright; + + # Reformat the HTML in the returned text. + # This probably wastes some bytes in the response, but it's + # easier to deal with stylistically. + $text = $passage->text; + $text = preg_replace('/\s+/', " ", $text); + $text = preg_replace('/class="(?:p|m)"/', + "class=\"scriptureText\"", $text); + $text = preg_replace('/class="s1"/', + "class=\"scriptureHeading\"", $text); + $text = preg_replace('/class="q1"/', + "class=\"scriptureText scripturePoetry1\"", $text); + $text = preg_replace('/class="q2"/', + "class=\"scriptureText scripturePoetry2\"", $text); + $title = $passage->display; + $return_text .= "

$title " . + "($version)

\n"; + $return_text .= $text; + $return_text .= ""; + + # This is why we wanted an array of the requests: so we could + # search through it and make sure the passages we received + # are the ones we were expecting. If there's an unfamiliar + # one, then it's possible the API is calling something by a + # different name (e.g. Ecclesiasticus == Sirach). + if (! array_search($title, $refs)) { + # If we don't recognize the title as one we requested, + # load the book tables and try to convert it to + # something canonical. + $title = ScriptureRequest::to_canonical($title); + } + + # Package the text for return. + $passage_obj = array( + 'title' => $title, + 'text' => $return_text, + 'version' => $version, + ); + # Put it in the object buffer. + $this->buffer[$title] = $passage_obj; + } + # Store the aggregated copyright data. + $this->copyright = $copyright_obj; + } + + /** + * Package all the received data for return. + */ + public function return_data() { + if (count($this->buffer)) { + $wrapper = array( + 'passages' => $this->buffer, + 'copyright' => $this->copyright + ); + } else { + $wrapper = []; + } + return $wrapper; + } +} diff --git a/sacred-wordpress.php b/sacred-wordpress.php new file mode 100644 index 0000000..1069325 --- /dev/null +++ b/sacred-wordpress.php @@ -0,0 +1,74 @@ + + * @copyright 2014 Joseph T. Richardson + * @license MIT License (http://opensource.org/licenses/MIT) + */ + +/** + * Plugin Name: Sacred WordPress + * Plugin URI: http://jtrichardson.com/projects/sacred-wordpress + * Description: A plugin to find and mark Scripture references in post content, retrieve Scripture text from a webservice API, and present it to the reader as a tooltip to the post reference. + * Version: 0.10.1 + * Author: Joseph T. Richardson + * Author URI: http://jtrichardson.com/ + * License: MIT License (http://opensource.org/licenses/MIT) + */ + +define("SACRED_WORDPRESS_VERSION", '0.10.1'); +require_once('tag_scripture.php'); +require_once('scripture.php'); + +// Add filters and actions +add_filter( 'the_content', 'tag_scriptures_in_the_content_filter' ); +add_filter( 'comment_text', 'tag_scriptures_in_comment_text_filter' ); +add_action( 'wp_enqueue_scripts', 'sacred_wordpress_enqueue_scripts' ); + +/** + * Implements WordPress filter 'the_content' + */ +function tag_scriptures_in_the_content_filter( $content ) { + $scriptureMarkup = new ScriptureMarkup($content); + $content = $scriptureMarkup->tag_text(); // in tag_scripture.php + return $content; +} + +/** + * Implements WordPress filter 'comment_text' + */ +function tag_scriptures_in_comment_text_filter( $comment_text ) { + $scriptureMarkup = new ScriptureMarkup($content); + $comment_text = $scriptureMarkup->tag_text(); // in tag_scripture.php + return $comment_text; +} + +/** + * Implements WordPress action 'wp_enqueue_scripts' + */ +function sacred_wordpress_enqueue_scripts() { + wp_enqueue_script('tooltipster', plugins_url('3rdparty/jquery.tooltipster.js', + __FILE__), array('jquery'), '3.2.6'); // This is Tooltipster version + + /* If you'd rather use the jsmin'ed version, you save only about + * 9K bytes in load. */ + wp_enqueue_script('scriptureRef', plugins_url('scriptureTooltip.js', + __FILE__), array('jquery', 'tooltipster'), SACRED_WORDPRESS_VERSION); + /* wp_enqueue_script('scriptureRef', plugins_url('scriptureTooltip.min.js', + __FILE__), array('jquery', 'tooltipster'), SACRED_WORDPRESS_VERSION); */ + + /* This is for the Fair Use Information System (FUMS) requested by the Bibles.org API. + * Should check this URL from time to time to keep current. + * @link http://bibles.org/pages/api/documentation/fums */ + wp_enqueue_script('biblesorg_fums', + 'http://d2ue49q0mum86x.cloudfront.net/include/fums.c.js', null, null); + + wp_enqueue_style('tooltipster-css', plugins_url('3rdparty/tooltipster.css', + __FILE__), null, '3.2.6'); + wp_enqueue_style('tooltipster-shadow-css',plugins_url('3rdparty/tooltipster-shadow.css', + __FILE__), array('tooltipster-css'), '3.2.6'); // This is Tooltipster version + wp_enqueue_style('scriptureTooltip-css', plugins_url('scriptureRef.css', + __FILE__), array('tooltipster-css', 'tooltipster-shadow-css'), SACRED_WORDPRESS_VERSION); +} diff --git a/sample-text.txt b/sample-text.txt new file mode 100644 index 0000000..97f5044 --- /dev/null +++ b/sample-text.txt @@ -0,0 +1,23 @@ +1996. Our justification comes from the grace of God. Grace is favor, the free and undeserved help that God gives us to respond to his call to become children of God, adoptive sons, partakers of the divine nature and of eternal life (cf. Jn 1:12-18; 17:3; Rom 8:14-17; 2 Pet 1:3-4). + +1997. Grace is a participation in the life of God. It introduces us into the intimacy of Trinitarian life: by Baptism the Christian participates in the grace of Christ, the Head of his Body. As an "adopted son" he can henceforth call God "Father," in union with the only Son. He receives the life of the Spirit who breathes charity into him and who forms the Church. + +1998. This vocation to eternal life is supernatural. It depends entirely on God's gratuitous initiative, for he alone can reveal and give himself. It surpasses the power of human intellect and will, as that of every other creature (cf. 1 Cor 2:7-9). + +1999. The grace of Christ is the gratuitous gift that God makes to us of his own life, infused by the Holy Spirit into our soul to heal it of sin and to sanctify it. It is the sanctifying or deifying grace received in Baptism. It is in us the source of the work of sanctification (cf. Jn 4:14; 7:38-39): Therefore if any one is in Christ, he is a new creation; the old has passed away, behold, the new has come. All this is from God, who through Christ reconciled us to himself (2 Cor 5:17-18). + +2000. Sanctifying grace is an habitual gift, a stable and supernatural disposition that perfects the soul itself to enable it to live with God, to act by his love. Habitual grace, the permanent disposition to live and act in keeping with God's call, is distinguished from actual graces which refer to God's interventions, whether at the beginning of conversion or in the course of the work of sanctification. + +2001. The preparation of man for the reception of grace is already a work of grace. This latter is needed to arouse and sustain our collaboration in justification through faith, and in sanctification through charity. God brings to completion in us what he has begun, "since he who completes his work by cooperating with our will began by working so that we might will it" (St. Augustine, De gratia et libero arbitrio, 17:PL 44,901): + +Indeed we also work, but we are only collaborating with God who works, for his mercy has gone before us. It has gone before us so that we may be healed, and follows us so that once healed, we may be given life; it goes before us so that we may be called, and follows us so that we may be glorified; it goes before us so that we may live devoutly, and follows us so that we may always live with God: for without him we can do nothing (St. Augustine, De natura et gratia, 31:PL 44,264). + +2002. God's free initiative demands man's free response, for God has created man in his image by conferring on him, along with freedom, the power to know him and love him. The soul only enters freely into the communion of love. God immediately touches and directly moves the heart of man. He has placed in man a longing for truth and goodness that only he can satisfy. The promises of "eternal life" respond, beyond all hope, to this desire: If at the end of your very good works ..., you rested on the seventh day, it was to foretell by the voice of your book that at the end of our works, which are indeed "very good" since you have given them to us, we shall also rest in you on the sabbath of eternal life (St. Augustine, Conf. 13,36 51:PL 32,868; cf. Gen 1:31). + +2003. Grace is first and foremost the gift of the Spirit who justifies and sanctifies us. But grace also includes the gifts that the Spirit grants us to associate us with his work, to enable us to collaborate in the salvation of others and in the growth of the Body of Christ, the Church. There are sacramental graces, gifts proper to the different sacraments. There are furthermore special graces, also called charisms after the Greek term used by St. Paul and meaning "favor," "gratuitous gift," "benefit" (Cf. LG 12). Whatever their character — sometimes it is extraordinary, such as the gift of miracles or of tongues — charisms are oriented toward sanctifying grace and are intended for the common good of the Church. They are at the service of charity which builds up the Church (cf. 1 Cor 12). + +2004. Among the special graces ought to be mentioned the graces of state that accompany the exercise of the responsibilities of the Christian life and of the ministries within the Church: Having gifts that differ according to the grace given to us, let us use them: if prophecy, in proportion to our faith; if service, in our serving; he who teaches, in his teaching; he who exhorts, in his exhortation; he who contributes, in liberality; he who gives aid, with zeal; he who does acts of mercy, with cheerfulness (Rom 12:6-8). + +2005. Since it belongs to the supernatural order, grace escapes our experience and cannot be known except by faith. We cannot therefore rely on our feelings or our works to conclude that we are justified and saved (cf. Council of Trent [1547]: DS 1533-1534). However, according to the Lord's words "Thus you will know them by their fruits" (Mt 7:20) — reflection on God's blessings in our life and in the lives of the saints offers us a guarantee that grace is at work in us and spurs us on to an ever greater faith and an attitude of trustful poverty. A pleasing illustration of this attitude is found in the reply of St. Joan of Arc to a question posed as a trap by her ecclesiastical judges: "Asked if she knew that she was in God's grace, she replied: 'If I am not, may it please God to put me in it; if I am, may it please God to keep me there'" (Acts of the trial of St. Joan of Arc). + +(Catechism of the Catholic Church, Second Edition. English translation © 1994, 1997, United States Catholic Conference. Original Latin text © 1993, Libreria Editrice Vaticana.) diff --git a/scripture.php b/scripture.php new file mode 100644 index 0000000..a48b9b5 --- /dev/null +++ b/scripture.php @@ -0,0 +1,390 @@ + 'BiblesOrg', + 'deutero_request' => 'BiblesOrg', + 'standard_version' => 'eng-ESV', + 'deutero_version' => 'eng-KJVA', + 'debug' => false, + ); + + /** + * This will assemble a query string of semicolon-separated Scripture + * references and send them to the APIs requested in $config. + * (Comma-separated also works with Bibles.org API, but I want to be + * able to keep compound references (e.g. Jn 3:3,5) together on our + * end, though they are broken down when sent to the APi.) + * + * @param The query string of references passed by the Ajax request.y + */ + public function get_passages($ref) { + global $BIBLEBOOKS; + + /* Expects JSON object in response from API request: + * { + * passages: { + * [ + * title: '', + * version: '', + * text: '' + * ] + * } + * copyright: { + * 'ESV': ... + * 'NKJV': ... + * } + * } + * Any specific API Request objects should implement this. + */ + + $refs = explode(";", $ref); + $refs = array_unique($refs); # Remove duplicate requests + + $proto_refs = array(); + $deutero_refs = array(); + + # Keep compound requests such as John 3:3,5 separate + # We will request the verses separately and reassmble later. + $compounds = array(); + + $whole_chapter = array(); + + while ($ref = array_shift($refs)) { + if (preg_match('/((?:\d )?(?:[A-Za-z ]+)) (\d.*)$/', + $ref, $matches)) { + $book = $matches[1]; + $verse = $matches[2]; # Everything to the end + } else { + # Then it will probably fail at the API level, too; + # Skip it, or we're crash the whole thing. + continue; + } + + /* Challenge: Bibles.org API returns a request for a whole + * chapter (e.g. 1 Corinthians 13) as a range of verses + * (i.e. 1 Corinthians 13:1-13). + * Our end needs to be able to understand that + * 1 Corinthians 13 === 1 Corinthians 13:1-13. + * Will give the script a table of chapters + * ($BIBLEBOOKS[CHAPTERS]) and the number + * of verses in them ($BIBLEBOOKS[CHAPTER_VERSES], + * but bible versions vary slightly in verse numbering + * and this method will not be absolute. + * Thankfully the API doesn't do this for a + * range of chapters (e.g. 1 John 1-2) */ + + # Identify such a case of a whole chapter reference + if (preg_match('/^\d+$/', $verse) and + $BIBLEBOOKS['CHAPTERS'][$book] != 1) { + $whole_chapter[] = $ref; + # So we will know to look for it later. + } + + # Convert compounds of adjacent verses (e.g. John 3:3,4) + # to a range. That's what we are really asking for` -- + # otherwise API will treat as two separate verses. + if (preg_match('/\b(\d+), ?(\d+)\b/', $verse, $matches) and + abs($matches[2] - $matches[1]) == 1) { + # Put them in the right order + if ($matches[2] < $matches[1]) { + $repl = "$matches[2]-$matches[1]"; + } else { + $repl = "$matches[1]-$matches[2]"; + } + # We do it this way rather than simply assign the string + # above in case this were a more complex reference, + # e.g. John 3:3,4,9 + $verse = str_replace($matches[0], $repl, $verse); + # Redefine the ref + $ref = "$book $verse"; + } + + # Break up other compounds + $verses = self::get_compound_parts($ref); + # Will return array of chapter-verses if compound + # (e.g 1 John 1:6, 8) + + if (self::is_deutero($book)) { + if ($verses) { # Then a compound reference + while ($v = array_shift($verses)) { + # Push each piece as a separate request, but + # remember it's a compound. + $compounds[] = $ref; + $deutero_refs[] = $v; + } + } else + $deutero_refs[] = $ref; + } else { + if ($verses) { # Then a compound reference + while ($v = array_shift($verses)) { + # Push each piece as a separate request, but + # remember it's a compound. + $compounds[] = $ref; + $proto_refs[] = $v; + } + } else + $proto_refs[] = $ref; + } + } + + if ($proto_refs) { + # Double-check for duplicates (may be new ones after compounding) + $proto_refs = array_unique($proto_refs); + + $ref = implode(';', $proto_refs); + + # A new object of the requested request + $request_class = self::$config['standard_request'] . '_Request'; + $req = new $request_class(); + $res1 = $req->get_passages($ref, self::$config['standard_version']); + } + + /* Have to make a separate request for deutero references since + * Protestant Bible translations (using the ESV by default) + * don't have them and API will return nothing. + */ + if ($deutero_refs) { + + # Double-check for duplicates (may be new ones after compounding) + $deutero_refs = array_unique($deutero_refs); + + $ref = implode(';', $deutero_refs); + + # A new object of the requested request + $request_class = self::$config['deutero_request'] . '_Request'; + $req = new $request_class(); + $res2 = $req->get_passages($ref, self::$config['deutero_version']); + } + + if (! isset($res1)) { + $res1 = ["passages" => [], "copyright" => []]; + } + if (! isset($res2)) { + $res2 = ["passages" => [], "copyright" => []]; + } + + $res = $this->merge_responses($res1, $res2); + $this->reassemble_compounds($res, $compounds); + + /* Now try to find those whole chapter requests + * @todo Need to make this part slightly more tolerant of variations in versification */ + + foreach ($whole_chapter as $ref) { + # First try to find how many verses were in that chapter + self::debug_print("Trying to find whole_chapter $ref\n"); + preg_match('/^([A-Za-z0-9 ]+) (\d+)$/', $ref, $matches); + $book = $matches[1]; + $chapter = $matches[2]; + $verses = $BIBLEBOOKS['CHAPTER_VERSES'][$book][$chapter-1]; + self::debug_print("Checking key '$book $chapter:1-$verses'\n"); + if ($verses) { + if (array_key_exists("$book $chapter:1-$verses", + $res["passages"])) { # e.g. 1 Corinthians 13:1-13 + self::debug_print("Found key '$book $chapter:1-$verses'\n"); + # Make an alias key for the whole chapter + $res["passages"]["$book $chapter"] = + $res["passages"]["$book $chapter:1-$verses"]; + } + } + } + + # Finally (and this is an afterthought), wrap each response in + # a div so jQuery can deal with it. + foreach ($res["passages"] as $ref => $passage) { + $res["passages"][$ref]['text'] = '
' . + $passage['text'] . '
'; + } + + return $res; + } + + /** + * A utility function to merge response objects. + * + * @param $res1 First of two response objects to merge. + * @param $res2 Second of two response objects to merge. + */ + private function merge_responses(array $res1, array $res2) { + $passages = array_merge($res1["passages"], $res2["passages"]); + $copyright = array_merge($res1["copyright"], $res2["copyright"]); + return array('passages' => $passages, 'copyright' => $copyright); + } + + /** + * A utility function to merge passages within responses + * (for use in reassembling compound references). + * + * @param $passages An array of passages to merge. + * @param $ref A compound reference that describes the assembled compound. + */ + private function merge_passages($passages, $ref) { + $new_passage = array(); + $new_passage['title'] = $ref; + $new_passage['version'] = $passages[0]['version']; + $new_passage['text'] = ''; + foreach ($passages as $passage) { + $new_passage['text'] .= $passage['text']; + } + return $new_passage; + } + + /** + * Reassemble the compound references (e.g. John 3:3,5) that we + * broke up before the request. Will take the array of compound + * references and pick the constituent verses out of the response. + * + * @param &$res Reference to the response object + * @param $compounds array of compounds + */ + private function reassemble_compounds(&$res, array $compounds) { + + $passages = $res["passages"]; + $passages_to_compound = array(); + + foreach ($compounds as $compound) { + $passages_to_compound = []; + # Will get an array of what verses to reassemble + $verses = self::get_compound_parts($compound); + + foreach ($verses as $verse) { + $passage = $passages[$verse]; + + # Remove the old head + $passage['text'] = preg_replace( + '/]* class="scriptureVerseHead[^>]*>(.*?)<\/h2>/', + '', $passage['text']); + + # Only want to return one scriptureVerseHead for each compound + if (! isset($verseHead)) { # Only do this once + $verseHead = '

' . + $compound . ' (' . + $passage['version'] . ')

'; + } + + if (! $passage) { + # Failed to receive expected part of compound + } else { + $passages_to_compound[] = $passage; + } + } + $compounded = + $this->merge_passages($passages_to_compound, $compound); + # Put the new head on it + $compounded['text'] = $verseHead . $compounded['text']; + $res["passages"][$compound] = $compounded; + } + } + + /** + * Break a compound reference into parts (individual verses). + * Returns an array of verses. + * + * @param $ref The compound reference to break up. + */ + static public function get_compound_parts($ref) { + if (preg_match('/((?:\d )?(?:[A-Za-z ]+)) (\d.*)$/', $ref, $matches)) { + $book = $matches[1]; + $verse = $matches[2]; # Everything to the end + } else { + throw new Exception("Unrecognized reference: $ref"); + } + + if (preg_match('/(\d+):(\d+(?:,\ ?\d+)+)$/', $verse, $matches)) { + $chapter = $matches[1]; + $verses_str = $matches[2]; + $verses = preg_split('/, ?/', $verses_str); + foreach ($verses as &$v) { + $v = "$book $chapter:$v"; + } + return $verses; + } + return 0; + } + + /** + * Is the book deuterocanonical? Check it against the table. + * + * @param $book A book name. + */ + static public function is_deutero($book) { + global $BIBLEBOOKS; + if (array_key_exists($book, $BIBLEBOOKS['DEUTERO'])) { + return 1; + } + return 0; + } + + /** + * Attempt to transform the reference into something canonical, + * given an unfamiliar book name (possibly an alternate name). + * E.g. The Bibles.org API returns the book of Sirach as + * Ecclesiasticus. Change it back to Sirach -- since that is + * what the requesting script is expecting. + * + * @param $ref A ref to canonicalize. + */ + static public function to_canonical($ref) { + global $BIBLEBOOKS; + if (! isset($BIBLEBOOKS['BOOKS'])) { + # Don't do this unless we have to + BibleBooks::prepare_book_maps(); + } + + if (preg_match('/((?:\d )?(?:[A-Za-z ]+)) (\d.*)$/', $ref, $matches)) { + $book = $matches[1]; + $verse = $matches[2]; + } + if ($BIBLEBOOKS['BOOKS'][$book]) { + $book = $BIBLEBOOKS['BOOKS'][$book]; + } + return "$book $verse"; + } + + /** + * Print if the debug flag is on. + * Only useful in accessing this script directly, since any stray + * text disrupts the JSON the other end is expecting. + * + * @param $string String to print. + */ + static public function debug_print($string) { + if (! ScriptureRequest::$config['debug']) { + return 0; + } + print $string; + } + + /** + * Constructor for ScriptureRequest object. + * Gets the query string from the CGI and triggers the request. + */ + public function __construct() { + # Set the debug flag if requested. + if (array_key_exists('debug', $_GET)) { + ScriptureRequest::$config['debug'] = 1; + } + + # Sanitize input + $reflist = filter_input(INPUT_GET, 'ref', FILTER_SANITIZE_STRING, + FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_LOW); + + if ($reflist) { + header("Content-type: application/json"); + $res = $this->get_passages($reflist); + print json_encode($res); + } + } +} /* class ScriptureRequest */ + +$scriptureReq = new ScriptureRequest(); diff --git a/scriptureRef.css b/scriptureRef.css new file mode 100644 index 0000000..4a816d5 --- /dev/null +++ b/scriptureRef.css @@ -0,0 +1,75 @@ +/** + * scriptureRef.css: Styling for scriptureTooltip.js's tooltips. + * + * @package Sacred Wordpress + */ + +span.scriptureRef { + color: navy; + text-decoration: underline; + cursor: pointer; +} + +.scriptureCopyright { + /* Just the 'copyright info' pseudo-link; + * actual copyright info will be tooltip */ + font-size: 0.8em; + margin-top: 0.6em; + margin-bottom: 0.7em; +} + +.scriptureText { + font-family: 'Old Style', serif; + margin-bottom: 0.5em; + margin-top: 0.6em; +} + +.scriptureText sup.v { + margin-left: 0.5ex; + margin-right: 0.5ex; + font-size: 0.7em; + font-family: 'Arial', sans-serif; +} + +.scripturePoetry1, .scripturePoetry2 { + margin-bottom: 0.5em; + margin-top: 0.5em; +} + +.scripturePoetry2 { + text-indent: 5ex; +} + +.scriptureVerseHead { + font-size: 1em; + /*font-family: 'Times New Roman', 'Transitional', sans-serif;*/ + font-family: 'Arial', sans-serif; + margin-bottom: 0.7em; + margin-top: 0.7em; +} + +.scriptureVerseHead .version { + font-size: .7em; +} + +.scriptureHeading { + font-size: 1em; + font-family: 'Times New Roman', 'Transitional', sans-serif; + margin-bottom: 0.7em; + margin-top: 0.7em; + font-weight: bold; +} + +.scriptureLinkMenuPanel { + float: right; + position: relative; + /*top: -10px;*/ + bottom: 5px; +} + +.scriptureLinkMenuTitle { + font-variant: small-caps; + font-size: 0.8em; + margin-top: 0px; + margin-bottom: 0px; +} diff --git a/scriptureTooltip.js b/scriptureTooltip.js new file mode 100644 index 0000000..bc63c9b --- /dev/null +++ b/scriptureTooltip.js @@ -0,0 +1,340 @@ +/** + * scriptureTooltip.js: Gathers Scripture references from page, requests + * from scripture.php, and generates tooltips (via jQuery + Tooltipster) + * of Scripture text. + * + * @package Sacred Wordpress + */ + +var scriptureTooltip = { + pathToPlugin: '/wp-content/plugins/sacred-wordpress', + debug: false, // Gives debugging output to console + + /** + * Default Bible site for reference to link to (from list in getLink). + * As it is set up, the Scripture reference in the tooltip is linked + * (the reference in the blog text is only styled like a link). + * @see getLink + */ + defaultScriptureSite: 'BibleGateway', + + /** + * URLs to copyright info for particular Bible versions we will use. + * Will be linked in tooltip if showCopyrightDataInTooltip is true. + */ + copyrightData: { + ESV: 'http://www.crossway.org/rights-permissions/esv/', + NASB: 'http://www.lockman.org/tlf/copyright.php' + }, + showCopyrightDataInTooltip: false, + + /** + * All the stuff to get it started. + */ + init: function() { + "use strict"; + // Gather list of the scriptures we need from the whole page + var neededScriptures = this.getScriptureList(); + // Preload scriptures from webservice + this.preloadScriptures(neededScriptures); + + /* Apply Tooltipster tooltips to all elements classed 'scriptureRef' + * (which should be those tagged by tag_scripture.php) + * For documentation of Tooltipster options, see + * @link http://iamceege.github.io/tooltipster/ + * I intend to also implement option to use jQuery UI tooltips + * instead of Tooltipster if you wish. + */ + /* Because WordPress loads jQuery in noConflict mode, we address + * jQuery by its full name. */ + jQuery(".scriptureRef").tooltipster({ + /* You're welcome to display something more creative while loading. + * In a long page of posts, it can take a few seconds to load. */ + content: "Loading...", + contentAsHTML: true, + maxWidth: 300, + theme: 'tooltipster-shadow', // You can customize the theme. + interactive: true, /* Allows a momentary delay for user to + * mouseover tooltip, in which case it then stays. */ + multiple: false + //autoClose: false // Useful to enable this for debugging DOM and CSS + }); + + if (this.debug) { + console.log("scriptureTooltip: All ready!"); + } + }, + + /** + * Gather the list of Scripture references in whole page from all + * elements classed 'scriptureRef' (applied by mark_scripture.php). + * The returned array will be passed to scripture.php to request + * Scripture text. + */ + getScriptureList: function () { + "use strict"; + if (this.debug) { + console.log("Getting scripture list.\n"); + } + var scriptureList = []; + + /* For each element classed 'scriptureRef', gather the reference + * into the array. */ + jQuery(".scriptureRef[ref]").each(function() { + scriptureList.push(jQuery(this).attr("ref")); + }); + + return scriptureList; + }, + + /** + * Make a single Ajax request at page load to get text of all + * Scripture references on page. This is slow up front but then allows + * immediate popup of all tooltips. + * Presumably someone actually reading will not jump immediately + * to requesting the text of a Scripture reference. + * @todo Allow an option to request texts separately on demand. + * + * @param scriptureList Array of Scripture references returned by getScriptureList(). + */ + preloadScriptures: function (scriptureList) { + "use strict"; + + /* We use semicolons to separate references here rather than commas + * since we want to treat compound references like John 3:3,5 + * as a single reference. */ + var queryString = scriptureList.join(";"); + + // Make the request. + jQuery.ajax({ + url: scriptureTooltip.pathToPlugin + + "/scripture.php" + "?ref=" + queryString, + type: "GET", + dataType: "JSON", + success: function(json) { + scriptureTooltip.setTooltips(json); + }, + error: function(jq, textStatus, errorThrown) { + if (scriptureTooltip.debug) { + console.log("scriptureTooltip: Ajax request failed!"); + console.log(textStatus); + console.log(errorThrown); + } + }, + complete: function(json) { + if (scriptureTooltip.debug) { + console.log("scriptureTooltip: Ajax request completed!"); + } + } + }); + }, + + /** + * Set the content of the tooltips with the Scripture text received + * from the Ajax request. + * + * @param json The JSON object we received from our request. + */ + setTooltips: function (json) { + "use strict"; + + jQuery(".scriptureRef[ref]").each(function() { + var ref, text, $html, url, version, $copyrightInfo; + + if (scriptureTooltip.debug) { + console.log(this); + } + + ref = jQuery(this).attr("ref"); + url = scriptureTooltip.getDefaultLink(ref); + + if (scriptureTooltip.debug) { + console.log("Getting ref: " + ref); + } + + /* If everything worked the way it should have in scripture.php, + * there ought to be a member matching 'ref' in the received + * JSON object... */ + if (json.passages.hasOwnProperty(ref)) { + text = json.passages[ref].text; + + /* Transmute hyphen to en dash for verse ranges, e.g. 1 Cor 9:10-12 + * (I'm a stickler for proper style) */ + text = text.replace(/\b(\d+)-(\d+)\b/, "$1\u2013$2"); + + /* Wrap the scriptureVerseHead (the reference at the top + * of the tooltip) in a link to the default Bible site. + * I would like to set a secondary tooltip here to show + * a menu of available Bible sites. */ + + $html = jQuery(text); + $html.find(".scriptureVerseHead") + .wrap(''); + + // An uglier way of doing that. + /* text = text.replace(/(]*? class="scriptureVerseHead"[^>]*?>)(.+?)(<\/h\2>)/, + "$1$3$4", text); */ + + version = json.passages[ref].version; + $copyrightInfo = scriptureTooltip.getCopyrightData( + version, json.copyright[version]); + if (scriptureTooltip.showCopyrightDataInTooltip) { + $html.find(".scriptureText:last").after($copyrightInfo); + } + jQuery(this).tooltipster("content", $html); + } else { + /* If there was not a matching 'ref' member in the JSON + * object, either the reference was incorrectly parsed, + * or it was an invalid reference, perhaps not Scripture + * at all. We should leave the ref alone and even strip + * the 'scriptureRef' class from the element. */ + jQuery(this).removeClass('scriptureRef'); + } + }); + }, + + /** + * Assemble the copyright data to include in the tooltip, + * if showCopyrightDataInTooltip is true. + * + * @param version The abbreviation for the version. + * @param versionText The copyright text returned by the API. + */ + getCopyrightData: function (version, versionText) { + "use strict"; + var copyright, copyrightURL; + + if (this.copyrightData.hasOwnProperty(version)) { + /* This is a URL to a page giving copyright info for the + * version. */ + copyrightURL = this.copyrightData[version]; + } else if (! versionText) { + /* If there's no copyright page for the version to link, + * and no copyright text returned from the API, then + * we've got nothing to show. + */ + return ''; + } + + /* This is ugly but it works. */ + if (copyrightURL) { + // We have a page to link to. + copyright = 'Copyright Information' + + (copyrightURL ? '' : ''); + copyright = '

' + copyright + '

'; + + return copyright; + }, + + /** + * This wraps each tagged scriptureRef in a link to the default + * Bible site. I decided not to do this (and instead to apply the + * link to the verse header in the tooltip) so mobile users could + * click the scriptureRef (styled like a link but not a link) + * to raise the tooltip, without leaving the page. + * We can probably refine this behavior (i.e. possibly link the + * scriptureRefs only when viewed by non-mobile users). + */ + setLinks: function () { + "use strict"; + jQuery(".scriptureRef").wrap(function() { + var ref, url; + ref = jQuery(this).attr("ref"); + url = this.getDefaultLink(ref); + return ''; + }); + }, + + /** + * Gets the default URL (at defaultScriptureSite) to link a + * scriptureRef to. + */ + getDefaultLink: function (ref) { + "use strict"; + // Give the standard URL to use as a link for the reference. + // May want to customize this to give user an option. + return this.getLink[this.defaultScriptureSite](ref); + }, + + /** + * Gets a link to a scriptureRef to several popular Bible sites. + * + * This is something I was working on to give a menu of links to + * various Bible sites in the tooltip. I will probably implement + * this as a secondary popup. + */ + getLink: { + BibleGateway: function (ref) { + "use strict"; + var baseUrl, url, version; + // e.g. https://www.biblegateway.com/passage/?search=Gen+1%3A1&version=RSV = Genesis 1:1 + baseUrl = 'https://www.biblegateway.com/passage/?search='; + version = 'RSVCE'; + url = baseUrl + ref.replace(' ', '+', 'g') + '&version=' + version; + return url; + }, + NewAdvent: function (ref) { + "use strict"; + var baseUrl, book, chapter, chapterstr, id, url, matches; + // e.g. http://www.newadvent.org/bible/sir003.htm = Sirach 3 + baseUrl = 'http://www.newadvent.org/bible/'; + + /* All books are first three letters of book name, lowercase + * with exceptions of Judith (jdt) and Philemon (phm) -- + * then three-digit chapter number + * i.e. mar016.htm for Mark 16 */ + + matches = ref.match(/((?:\d )?(?:[A-Za-z ]+)) (\d+):?/); + book = matches[1]; + chapter = matches[2]; + book = book.replace(/ /, '', 'g'); + if (chapter < 10) { + chapterstr = '00' + chapter; + } else if (chapter < 100) { + chapterstr = '0' + chapter; + } else { + chapterstr = chapter; + } + if (book === 'Judith') { + id = 'jdt'; + } else if (book === 'Philemon') { + id = 'phm'; + } else { + id = book.substr(0, 3).toLowerCase(); + } + url = baseUrl + id + chapterstr + '.htm'; + return url; + } + /* Some more sites I was thinking of adding: + * + * BibleStudyTools: + * e.g. http://www.biblestudytools.com/genesis/passage.aspx?q=genesis+1:1-5 + * = Genesis 1:1-5 + * + * Biblia: + * e.g. http://biblia.com/bible/nrsv/Sir3.30 = Sirach 3:30 + * http://biblia.com/books/nrsv/Ge1.1-5 = Genesis 1:1-5 + * + * BibleHub: + * e.g. http://biblehub.com/genesis/1-5.htm = Genesis 1:5 + * + * and more! + */ + } +}; + +/* Because WordPress loads jQuery in noConflict mode, we address + * jQuery by its full name. */ +jQuery(document).ready(function() { + "use strict"; + scriptureTooltip.init(); +}); + +// Debugging strings for JSLint, JSHint, etc. +/* global jQuery: false */ +/* jslint browser: true, devel: true, unparam: true, todo: true, white: true */ diff --git a/scriptureTooltip.min.js b/scriptureTooltip.min.js new file mode 100644 index 0000000..7141fc6 --- /dev/null +++ b/scriptureTooltip.min.js @@ -0,0 +1,12 @@ +// scriptureTooltip.js + +var scriptureTooltip={pathToPlugin:'/wp-content/plugins/sacred-wordpress',debug:false,defaultScriptureSite:'BibleGateway',copyrightData:{ESV:'http://www.crossway.org/rights-permissions/esv/',NASB:'http://www.lockman.org/tlf/copyright.php'},showCopyrightDataInTooltip:false,init:function(){"use strict";var neededScriptures=this.getScriptureList();this.preloadScriptures(neededScriptures);jQuery(".scriptureRef").tooltipster({content:"Loading...",contentAsHTML:true,maxWidth:300,theme:'tooltipster-shadow',interactive:true,multiple:false});if(this.debug){console.log("scriptureTooltip: All ready!");}},getScriptureList:function(){"use strict";if(this.debug){console.log("Getting scripture list.\n");} +var scriptureList=[];jQuery(".scriptureRef[ref]").each(function(){scriptureList.push(jQuery(this).attr("ref"));});return scriptureList;},preloadScriptures:function(scriptureList){"use strict";var queryString=scriptureList.join(";");jQuery.ajax({url:scriptureTooltip.pathToPlugin+"/scripture.php"+"?ref="+queryString,type:"GET",dataType:"JSON",success:function(json){scriptureTooltip.setTooltips(json);},error:function(jq,textStatus,errorThrown){if(scriptureTooltip.debug){console.log("scriptureTooltip: Ajax request failed!");console.log(textStatus);console.log(errorThrown);}},complete:function(json){if(scriptureTooltip.debug){console.log("scriptureTooltip: Ajax request completed!");}}});},setTooltips:function(json){"use strict";jQuery(".scriptureRef[ref]").each(function(){var ref,text,$html,url,version,$copyrightInfo;if(scriptureTooltip.debug){console.log(this);} +ref=jQuery(this).attr("ref");url=scriptureTooltip.getDefaultLink(ref);if(scriptureTooltip.debug){console.log("Getting ref: "+ref);} +if(json.passages.hasOwnProperty(ref)){text=json.passages[ref].text;text=text.replace(/\b(\d+)-(\d+)\b/,"$1\u2013$2");$html=jQuery(text);$html.find(".scriptureVerseHead").wrap('');version=json.passages[ref].version;$copyrightInfo=scriptureTooltip.getCopyrightData(version,json.copyright[version]);if(scriptureTooltip.showCopyrightDataInTooltip){$html.find(".scriptureText:last").after($copyrightInfo);} +jQuery(this).tooltipster("content",$html);}else{jQuery(this).removeClass('scriptureRef');}});},getCopyrightData:function(version,versionText){"use strict";var copyright,copyrightURL;if(this.copyrightData.hasOwnProperty(version)){copyrightURL=this.copyrightData[version];}else if(!versionText){return'';} +if(copyrightURL){copyright='Copyright Information'+ +(copyrightURL?'':'');copyright='

'+copyright+'

';return copyright;},setLinks:function(){"use strict";jQuery(".scriptureRef").wrap(function(){var ref,url;ref=jQuery(this).attr("ref");url=this.getDefaultLink(ref);return'';});},getDefaultLink:function(ref){"use strict";return this.getLink[this.defaultScriptureSite](ref);},getLink:{BibleGateway:function(ref){"use strict";var baseUrl,url,version;baseUrl='https://www.biblegateway.com/passage/?search=';version='RSVCE';url=baseUrl+ref.replace(' ','+','g')+'&version='+version;return url;},NewAdvent:function(ref){"use strict";var baseUrl,book,chapter,chapterstr,id,url,matches;baseUrl='http://www.newadvent.org/bible/';matches=ref.match(/((?:\d )?(?:[A-Za-z ]+)) (\d+):?/);book=matches[1];chapter=matches[2];book=book.replace(/ /,'','g');if(chapter<10){chapterstr='00'+chapter;}else if(chapter<100){chapterstr='0'+chapter;}else{chapterstr=chapter;} +if(book==='Judith'){id='jdt';}else if(book==='Philemon'){id='phm';}else{id=book.substr(0,3).toLowerCase();} +url=baseUrl+id+chapterstr+'.htm';return url;}}};jQuery(document).ready(function(){"use strict";scriptureTooltip.init();}); \ No newline at end of file diff --git a/tag_scripture.php b/tag_scripture.php new file mode 100644 index 0000000..bff59ee --- /dev/null +++ b/tag_scripture.php @@ -0,0 +1,422 @@ +tag_text($text); + echo $scriptureMarkup->get_text(); + } +} +/* Otherwise this file only works when called directly by WordPress. */ + +class ScriptureMarkup { + private $last_book; + private $last_chapter; + private $text; + private $debug = false; + + public function __construct($text) { + $this->last_book = ''; + $this->last_chapter = 0; + + $this->text = $this->tag_text($text); + } + + /** + * Return the marked-up text. + */ + public function get_text() { + return $this->text; + } + + /** + * Tag the Scripture references in the text (either provided in + * constructor or passed as parameter here. + * + * @param $text Optional passing of text to tag. + */ + public function tag_text($text = null) { + if (! $text) { + $text = $this->text; + } + + /* If it hasn't been already, prepare the table of books. */ + if (! isset($BIBLEBOOKS['BOOKS'])) { + BibleBooks::prepare_book_maps(); + } + + $count = 0; + + if ($this->debug) { + echo "\n"; + } + + /* This should match: + * "1 Corinthians 13:1" + * "1 Corinthians 13:1-3" + * "1 Corinthians 13:1,3" + * "1 Corinthians 13" + * If a chapter was previously referred to, will match: + * v. 1 OR vv. 1-3 + * Should match in the same citation: 1 Corinthians 13:1,3 + * Should match in DIFFERENT citations: 1 Corinthians 13:1, 15:2 */ + + $text = preg_replace_callback( + "/\b # Match a word boundary -- + # shouldn't be a part of anything else + (?: + ( # First capture group: The name of the book + # The numeric prefix, e.g. *1* Corinthians + (?:(?:[1-4]|(?:I|II|III|IV)|(?:1st|2nd|3rd|4th) + |(?:First|Second|Third|Fourth))\b\ )? + + # The first word of the title, first letter capital + (?:[A-Z][a-z]+\b\.?) + # Additional words of the title (up to two more) + (?:\ (?:[oO]f(?:\ [tT]he)?|[A-Z][a-z]+)\b\.?){0,2} + # Must begin in capital or be the word 'of' + ) + \s? # A space between the book name and the chapter-verse + ( # Second capture group: The chapter-verse + (?: + (?: # A chapter-verse reference or range + # A basic chapter-verse reference, e.g. 3:16 + \d+[:.]\d+ + # With a possible range of verses (e.g. 3:16-19) + (?:[-\x{2013}] # \x{2013} = EN DASH + # End verse of a range, + # possibly in another chapter + (?:\d+[:.])?\d+)? + | + # Or can be a whole chapter + # (or a verse in single-chapter book) + \d+ + # Or a possible range of chapters + (?:[-\x{2013}]\d+)? + ) + (?:,\ * # A comma or other separator between refs + |\ and\ # Allow for cases e.g. 'John 6:37 and 10:27–30x' + |\ or\ + )? + # But don't match the next one if it appears to be + # another book. + (?!\ (?:[1-4]\ )?[A-Z][a-z]+) + )+ # But if possible, match a whole string of refs + ) + | + ( # Even if we don't find a book name, there still + # might be something to match + # (as third capture group) + (?<=v\.\ ) # Look-behind to v. (verse) or vv. (verses) + \d+ # A verse number + (?:[-\x{2013}] # Or possible range of verses + \d+ + )? + (?:,\ ?\d+ # Or single verses separated by commas + (?!\ +[A-Z][a-z]+) # But not another book name + )* + ) + ) + \b + (?![^<]+>) # This should make sure we weren't in the middle + # of an HTML tag and accidentally match things + # like Web and IP addresses + /xu", + array($this, 'mark_scripture'), $text, -1, $count); + + if ($this->debug) { + echo "\n"; + } + return $text; + } + + /** + * The callback method for preg_replace_callback() in tag_text(). + * This cuts a string of apparent Scripture references into pieces, + * determines if they are actually Scripture references after all, + * and if they are, tags them appropriately and returns the tagged + * string to be replaced in the text. + * + * @param $matches The matches passed by preg_replace_callback(). + */ + private function mark_scripture($matches) { + if ($this->debug) { + print "\n"; + } + + if (isset ($matches[3])) { + # Then it's something we recognized as a Scripture reference + # without a book or chapter (e.g. "v." or "vv.") + $book = ''; + $verses = array($matches[3]); + } else { + # We presume $matches[1] and $matches[2] matched: a book + # reference followed by a string of verse references + + $book = $matches[1]; + # Split comma-separated references into array + $versestr = preg_replace('/ and | or /', ', ', $matches[2]); + # 'and' or 'or' in the string is really a list separator + # e.g. John 6:37–40 and 10:27–30 + $verses = preg_split('/(?:,\ *)(?=\d+[.:]|$)/', $versestr); + # Look-ahead to be sure we don't split at compound verse + # references, e.g. John 3:3,5. + # Only split at full chapter:verse references, + # e.g. John 3:3, 6:35 + if ($this->debug) { + echo "\n"; + } + } + + if (! $book) { + $book = ''; + } + $return_string = $matches[0]; # The whole matched string + $i = -1; + foreach ($verses as $verse) { + $i++; # So will be 0 on first iteration (index of $verses) + if ($this->debug) { + print "\n"; + } + $flags = array(); + if ($this->debug) { + print "\n"; + } + if (! $book) { + # If we didn't match a book name, + # then assume the last named reference + if (! $this->last_book) { goto FAIL; } + if ($this->debug) { + print "\n"; + } + $book = $this->last_book; + $flags[] = "book_inferred"; + + if ($this->is_chapterless($book, $verse)) { # e.g. v. 5 + if ($this->debug) { + print "\n"; + } + if (! $this->last_chapter) { goto FAIL; } + $verse = $this->last_chapter . ":$verse"; + $flags[] = "chapter_inferred"; + } + } # If no $last_book set, i.e. if this is first possible + # reference, but there is o book name, this will fail + $ref = $this->is_it_scripture($book, $verse, $flags); + if ($ref) { + $flagstr = implode(" ", $flags); + if ($matches[1]) { $this->last_book = $book; } + if ($i == 0) { # The first one, with the book reference + $replace = "" . + ($matches[1] ? $matches[1] . " " : "") . + $verse . ""; + $return_string = str_replace( + # Also match the book reference here if it exists + ($matches[1] ? $matches[1] . " " : "") . $verse, + $replace, $return_string); + } else { # Just the verse reference + $replace = "" . $verse . ""; + $return_string = str_replace($verse, $replace, $return_string); + } + if ($this->debug) { + print "\n"; + } + } else { # It's probably not a Scripture reference after all + FAIL: + # Don't replace anything; leave it as it is + } + } + if ($this->debug) { + print "\n\n"; + } + return $return_string; + } + + /** + * Passed an apparent Scripture reference, determines if it is + * actually Scripture after all. + * + * @param $book The book string matched in the regexp. + * @param $verse The chapter-verse string. + * @param &$flags Flags about the string we are passing around. + */ + private function is_it_scripture($book, $verse, &$flags) { + global $BIBLEBOOKS; + if ($this->debug) { + print "\n"; + } + if (! $book) { + # If there's no book, not even an inferred one, then we + # obviously have nothing to do here. + return 0; + } + # The cases when people spell out "First Corinthians" or use + # Roman or ordinal numerals are few and far between, but it's + # worthwhile to catch them. + $book = preg_replace('/^(I|1st|First)\b/', '1', $book); + $book = preg_replace('/^(II|2nd|Second)\b/', '2', $book); + $book = preg_replace('/^(II|3rd|Third)\b/', '3', $book); + $book = preg_replace('/^(II|4th|Fourth)\b/', '4', $book); + $book = preg_replace('/\./', '', $book); + if ($this->debug) { + print "\n"; + } + return $ref; + } else { + # It wasn't Scripture after all. + if ($this->debug) { + print "NOT SCRIPTURE -->\n"; + } + return 0; + } + } + + /** + * Is this a book composed of a single chapter? (i.e. Jude, 2 John) + * + * @param $book The name of a Bible book. + */ + private function is_single_chapter($book) { + global $BIBLEBOOKS; + $book = $BIBLEBOOKS['BOOKS'][$book]; # The canonical book name + if ($BIBLEBOOKS['CHAPTERS'][$book] == 1) { + return 1; + } + } + + /** + * Given a book and verse reference, is this a reference to verses + * without a chapter reference? (e.g. 9-10, not 5:9-10) + * (We already know that there was no actual book reference, + * only an inferred book -- since $matches[1] did not match above). + * + * @param $book The inferred Bible book. + * @param $verse The chapter-verse string + */ + private function is_chapterless($book, $verse) { + if ($this->is_single_chapter($book)) { + return 0; # Because Chapter 1 will be inferred later + } + if (preg_match('/[:.]/', $verse, $matches)) { + return 0; # Then an explicit chapter is given here, e.g. 13:10-15 + } + # Otherwise -- something like 15 or 10-15. + return 1; + } + + /** + * Identify the chapter element of the reference + * for use in forward inferences. + * (This doesn't actually return the last chapter, but sets the + * $last_chapter for use in the future.) + * + * @param $book The book string. + * @param $verse The chapter-verse string. + */ + function last_chapter($book, $verse) { + # + global $BIBLEBOOKS; + # Get the last chapter referred to + $book = $BIBLEBOOKS['BOOKS'][$book]; + if ($BIBLEBOOKS['CHAPTERS'][$book] == 1) { + # Only one chapter in this book, so no chapter reference. + # We can quit already. + return 1; + } + # This regexp takes a string like 9:10-9:15, 9:10-15, etc. + # and picks out the chapter part. + if (preg_match('/(\d+)([:.]\d+)?(?:[-\x{2013}](\d+)([:.]\d+))?/u', + $verse, $matches)) { + $begin_chapter = ''; + $begin_verse = ''; + $end_chapverse = ''; + $end_verse = ''; + if (array_key_exists(1, $matches)) { + $begin_chapter = $matches[1]; + } + if (array_key_exists(2, $matches)) { + $begin_verse = $matches[2]; + } + if (array_key_exists(3, $matches)) { + $end_chapverse = $matches[3]; + } + if (array_key_exists(4, $matches)) { + $end_verse = $matches[4]; + } + if (! $end_chapverse or ! $end_verse) { + # Either 3:3 OR 3:3-5 + return $begin_chapter; + } else { # 3:3-4:1 + return $end_chapverse; + } + } + } +}