!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.O=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o", _, arguments); }, update: function() { console.log("STATE (.)", _, arguments); }, exit: function() { console.log("STATE <=", _, arguments); } }); }; return _debug; } module.exports = Debug; },{"../story":14}],4:[function(_dereq_,module,exports){ var Action = _dereq_('../story').Action; var Audio = function(el){ return { play: function() { return Action(function() { el.play() }); }, pause: function() { return Action(function() { el.pause() }); }, setCurrentTime: function(t) { return Action(function() { el.currentTime = t; }); } } }; module.exports = Audio; },{"../story":14}],5:[function(_dereq_,module,exports){ module.exports = { Sleep: _dereq_('./sleep'), Debug: _dereq_('./debug'), Location: _dereq_('./location'), Audio: _dereq_('./html5audio'), Leaflet: { Marker: _dereq_('./leaflet/marker'), Map: _dereq_('./leaflet/map'), Popup: _dereq_('./leaflet/popup') }, CSS: _dereq_('./css'), Slides: _dereq_('./slides'), MiniProgressBar: _dereq_('./mini_progressbar') }; },{"./css":2,"./debug":3,"./html5audio":4,"./leaflet/map":6,"./leaflet/marker":7,"./leaflet/popup":8,"./location":9,"./mini_progressbar":10,"./sleep":11,"./slides":12}],6:[function(_dereq_,module,exports){ var Action = _dereq_('../../story').Action; function MapActions(map) { function _map() {} // helper method to translate leaflet methods to actions function leaflet_method(name) { _map[name] = function() { var args = arguments; return Action(function() { map[name].apply(map, args); }); }; } function leaflet_move_method(name, signal) { _map[name] = function() { var args = arguments; return Action({ enter: function() { this.moveEnd = function() { map.off(signal, this.moveEnd, this); this.finish(); }; map.on(signal, this.moveEnd, this); map[name].apply(map, args); return true; }, clear: function() { map.off(signal, this.moveEnd, this); } }); }; } _map.moveLinearTo = function(from, to, options) { var opt = options || { k: 2 }; var animationTimer; var posTarget; var delta = 20; // leaflet map animation is not being used because // when the action is updated the map just stop the animatin // and start again // the animtion should be smooth return Action({ enter: function () { posTarget = map.project(map.getCenter()); animationTimer = setInterval(function() { var c = map.project(map.getCenter()); var px = c.x + (posTarget.x - c.x)*delta*0.001* opt.k; var py = c.y + (posTarget.y - c.y)*delta*0.001* opt.k; map.panTo(map.unproject(L.point(px, py)), { animate: false }); }, delta); }, update: function (t) { var p0 = map.project(from); var p1 = map.project(to); posTarget = p0.add(p1.subtract(p0).multiplyBy(t)); }, exit: function() { clearInterval(animationTimer); }, clean: function() { this.exit(); } }); }; // leaflet methods leaflet_move_method('panTo', 'moveend'); leaflet_move_method('setView', 'moveend'); leaflet_move_method('setZoom', 'zoomend'); return _map; } if (typeof window.L !== 'undefined') { L.Map.addInitHook(function () { this.actions = MapActions(this); }); } module.exports = MapActions; },{"../../story":14}],7:[function(_dereq_,module,exports){ var Action = _dereq_('../../story').Action; function MarkerActions(marker) { function _marker() {} _marker.addTo = function(map) { return Action(function() { marker.addTo(map); }); }; _marker.addRemove = function(map) { return Action({ enter: function() { marker.addTo(map); }, exit: function() { map.removeLayer(marker); }, clear: function() { map.removeLayer(marker); } }); }; _marker.icon = function(iconEnabled, iconDisabled) { iconEnabled = L.icon({ iconUrl: iconEnabled }); iconDisabled = L.icon({ iconUrl: iconDisabled }); return Action({ enter: function() { marker.setIcon(iconEnabled); }, exit: function() { marker.setIcon(iconDisabled); }, clear: function() { } }); } return _marker; } function PathActions(path) { function _path() {}; _path.toggleStyle = function(styleDisabled, styleEnabled) { return Action({ enter: function() { path.setStyle(styleEnabled); }, exit: function() { path.setStyle(styleDisabled); }, clear: function() { } }); } return _path; } if (typeof window.L !== 'undefined') { L.Marker.addInitHook(function () { this.actions = MarkerActions(this); }); L.Path.addInitHook(function () { this.actions = PathActions(this); }) } module.exports = MarkerActions; //marker.actions.addTo(map); //addState(, map.actions.moveTo(..).addMarker(m) },{"../../story":14}],8:[function(_dereq_,module,exports){ /** directional popup allows to create popups in the left and the right of a point, not only on the top. Same api than L.popup but you can spcify ``potision`` to 'left' or 'right' */ var Action = _dereq_('../../story').Action; if (typeof window.L !== 'undefined') { var DirectionalPopup = L.Popup.extend({ _updatePosition: function() { L.Popup.prototype._updatePosition.call(this); var offset = L.point(this.options.offset), animated = this._animated; switch(this.options.position) { case 'left': this._container.style.bottom = 'auto'; this._container.style.left = 'auto'; this._container.style.right = offset.x + (animated ? 0 : pos.x) + "px"; break; case 'right': this._container.style.bottom = 'auto'; this._container.style.left = offset.x + "px"; break; } } }); L.DirectionalPopup = DirectionalPopup; L.directionalPopup = function (options, source) { return new L.DirectionalPopup(options, source); }; function PopupActions(popup) { function _popup() {} // helper method to translate leaflet methods to actions function leaflet_method(name) { _popup[name] = function() { var args = arguments; return Action(function() { popup[name].apply(popup, args); }); }; } // leaflet methods leaflet_method('openOn'); _popup.openClose = function(map) { if (!map) { throw new Error("openClose gets map as first param"); } return Action({ enter: function() { popup.openOn(map); }, exit: function() { map.closePopup(popup); } }); }; return _popup; } L.Popup.addInitHook(function () { this.actions = PopupActions(this); }); } },{"../../story":14}],9:[function(_dereq_,module,exports){ var Action = _dereq_('../story').Action; var loc = window.location; var Location = { // changes the browser url hash changeHash: function(hash) { if (hash === undefined) throw new Error("hash should be a string"); return Action(function() { loc.hash = hash; }); } }; module.exports = Location; },{"../story":14}],10:[function(_dereq_,module,exports){ var Action = _dereq_('../story').Action; /** * mini progress var adds a small line to the top of the browser * and moves according to story progress * usage: * var pg = MiniProgressBar() * story.addAction(trigger, pg.percent(10)) //goes to 10% */ var MiniProgressBar = function(el) { var defaultStyle = { position: 'fixed', left: 0, top: 0, height: '3px', display: 'inline-block', background: '#ff7373', 'z-index': 2 }; var pg = {}; // create an element and apply default style var div = document.createElement('div'); div.setAttribute('class', 'oddysey-miniprogressbar'); for (var s in defaultStyle) { div.style[s] = defaultStyle[s]; } // append to element or to tge body (el || document.body).appendChild(div); /** * returns an action that moves the percentaje bar to the specified one */ pg.percent = function(p) { return Action(function() { div.style.width = p + "%"; }); }; return pg; } module.exports = MiniProgressBar; },{"../story":14}],11:[function(_dereq_,module,exports){ var Action = _dereq_('../story').Action; function Sleep(ms) { return Action({ enter: function() { setTimeout(this.finish, ms); return true; } }); } module.exports = Sleep; },{"../story":14}],12:[function(_dereq_,module,exports){ var Action = _dereq_('../story').Action; var Core = _dereq_('../core'); var classList = _dereq_('../util/classList'); function Slides(el) { function slides() {}; function _activate(idx) { var slideElements = Core.getElement(el).children; for(var i = 0; i < slideElements.length; ++i) { if (i === idx) { slideElements[i].classList.add("selected", "selected_slide"); } else { slideElements[i].classList.remove("selected", "selected_slide"); } } } slides.activate = function(i) { return Action(function() { _activate(i); }); }; _activate(-1); return slides; } module.exports = Slides; },{"../core":13,"../story":14,"../util/classList":24}],13:[function(_dereq_,module,exports){ function getElement(el) { if(typeof jQuery !== 'undefined') { if (el instanceof jQuery) { return el[0]; } else if(typeof el === 'string') { if (el[0] === '#' || el[0] === '.') { return getElement($(el)); } } } if (el instanceof NodeList || el instanceof HTMLCollection) { return el[0]; } else if (el instanceof Element) { return el; } return document.getElementById(el); } module.exports = { getElement: getElement }; },{}],14:[function(_dereq_,module,exports){ _dereq_('../../vendor/d3.custom'); function Story() { var triggers = []; var events = []; var currentState = null; var prevState = null; function story(t) { } // event non attached to states story.addEvent = function(trigger, action) { trigger._story(story, function() { action.enter(); }); events.push({ a: trigger, b: action }); return story; }; story.clear = function() { var all = triggers.concat(events); for(var i = 0; i < all.length; ++i) { var a = all[i]; a.a.story = null; a.a.trigger = null; a.a.clear && a.a.clear(); a.b.clear && a.b.clear(); } triggers = []; events = []; currentState = null; prevState = null; }; // go to state index story.go = function(index, opts) { opts = opts || {}; if(index < 0 && index > triggers.length) { throw new Error("index should be less than states length"); } if (story.state() !== index) { if (opts.reverse) { var a = triggers[index].a; if (a.reverse) { a.reverse(); } } // current state story.state(index); // raise exit if (prevState !== null) { var prev = triggers[prevState].b; if (prev.exit) { prev.exit(); } } var b = triggers[index].b; // enter in current state b.enter(); } }; story.addState = function(a, b, opts) { var i = triggers.length; if(!a || !b) { throw new Error("action and trigger must be defined"); } triggers.push({ a: a, b: b, opts: opts }); a._story(story, function() { story.go(i); }); return story; }; story.addLinearState = function(a, b, opts) { var j; var i = triggers.length; triggers.push({ a: a, b: b, opts: opts }); a._story(story, function(t) { if (story.state() !== i) { story.go(i); } else { if (b.update) { b.update(t); } } }); return story; }; story.state = function(_) { if(_ === undefined) return currentState; prevState = currentState; currentState = _; return; }; return story; } // // basic action // t can be a function or an object // if is a function it's called on ``enter`` event // if t is an object with enter/exit/update methods // they're called on state changes function Action(t) { var evt = d3.dispatch('finish'); var action = t; if (t.enter === undefined && !(typeof(t) === 'function' && t.prototype.enter !== undefined)) { action = { enter: t } } return d3.rebind(action, evt, 'on', 'finish'); } function Trigger(t) { t = t || {} t._story = function(story, trigger) { this.trigger = trigger; this.story = story; }; t.then = function(t, context) { this.trigger = function() { if (t.trigger) { t.trigger(); if (t.reverse) t.reverse(); } else if (t.call) { t.call(context || self); } else { throw new Error("then first param should be either function or trigger"); } }; }; return t; } /// // executes actions in parallel // usage: // Parallel(action1, action2, action3) // // raises finish when all the tasks has been completed // function Parallel () { var actions = Array.prototype.slice.call(arguments); var tasksLeft; function _Parallel() {} function start() { tasksLeft = actions.length; } function done() { if (--tasksLeft === 0) { _Parallel.finish(); } } function wait(action) { action.on('finish.parallel', function() { action.on('finish.parallel', null); done(); }); } _Parallel.enter = function() { start(); for(var i = 0, len = actions.length; i < len; ++i) { var a = actions[i]; if (a.enter) { if (a.enter()) { wait(a); } else { done(); } } } }; _Parallel.exit = function() { start(); for(var i = actions.length - 1; i >= 0; --i) { var a = actions[i]; if (a.exit) { if (a.exit()) { wait(a); } else { done(); } } } }; _Parallel.clear = function() { for(var i = 0, len = actions.length; i < len; ++i) { var a = actions[i]; a.clear && a.clear(); } } _Parallel = Action(_Parallel); return _Parallel; } /// // executes actions serially, waits until the previous task // is completed to start with the second and so on // usage: // Step(action1, action2, action3) // // raises finish when all the tasks has been completed // function Step() { var actions = Array.prototype.slice.call(arguments); var queue; function _Step() {} function next(method) { if (queue.length === 0) { _Step.finish(); return; } var a = queue.pop(); if (a.on) { a.on('finish.chain', function() { a.on('finish.chain', null); next(method); }); } if (!a[method] || !a[method]()) { next(method); if(a.on) a.on('finish.chain', null); } } _Step.enter = function() { // call enter on each action queue = actions.slice().reverse(); next('enter'); return true; } _Step.exit = function() { // call exit on each action queue = actions.slice(); next('exit'); return true; } _Step.clear = function() { for(var i = 0, len = actions.length; i < len; ++i) { var a = actions[i]; a.clear && a.clear(); } } _Step = Action(_Step); return _Step; } // check change between two states and triggers function Edge(a, b) { var s = 0; function t() {} a._story(null, function() { if(s !== 0) { t.trigger(); } s = 0; }); b._story(null, function() { if(s !== 1) { t.trigger(); } s = 1; }); return Trigger(t); } module.exports = { Story: Story, Action: Action, Trigger: Trigger, Step: Step, Parallel: Parallel, Edge: Edge }; },{"../../vendor/d3.custom":32}],15:[function(_dereq_,module,exports){ _dereq_('../../vendor/markdown'); var mapActions = { 'move map to current position': function() { var center = this.map.getCenter() return '- center: [' + center.lat.toFixed(4) + ', ' + center.lng.toFixed(4) + ']\n- zoom: ' + this.map.getZoom() }, 'show marker at current position': function() { var center = this.map.getCenter() return 'L.marker([' + center.lat.toFixed(4) + ', ' + center.lng.toFixed(4) + ']).actions.addRemove(S.map)'; }, 'sleep': function() { return "O.Actions.Sleep(1000)"; } }; var Template = function(template) { var initialized = false; function readMessage(event) { var msg = JSON.parse(event.data); template.editor = true; if (!initialized) { configureEditor(); initialized = true; } function sendMsg(_) { event.source.postMessage(JSON.stringify({ id: msg.id, data: _ }), event.currentTarget ? event.currentTarget.location : event.source.location); } if (msg.type === 'md') { var actions = actionsFromMarkdown(msg.code); try { template.update(actions); sendMsg(null); } catch(e) { sendMsg(e.message); } } else if (msg.type === 'actions') { var actions = []; if (template.map && template.map instanceof L.Map) { for (var k in mapActions) { actions.push(k); } } if (template.actions) { for (var k in template.actions) { actions.push(k); } } sendMsg(actions); } else if (msg.type === 'get_action') { if (msg.code in mapActions) { sendMsg(mapActions[msg.code].call(template)); } else { sendMsg(template.actions[msg.code].call(template)); } } else if (msg.type === 'code') { sendMsg(eval(msg.code)); } else if (msg.type === 'change_slide') { template.changeSlide && template.changeSlide(msg.slide); } } if (!window.addEventListener) { window.attachEvent("message", function load(event) { readMessage(event); }); } else { window.addEventListener("message", function load(event) { readMessage(event); }); } template.init(function() { }); function configureEditor() { // add helpers if (template.map && template.map instanceof L.Map) { new L.CrossHair('//cartodb.github.io/odyssey.js/img/crosshair.png').addTo(template.map); } } window.onload = function() { var origin = location.pathname.split('/')[1]; Template.Storage.load(function(md) { template.update(actionsFromMarkdown(md)); }); } }; Template.Storage = { save: function(md, template) { location.hash = "md/" + template + "/" + btoa(md); }, load: function(done) { if (document.getElementById('md_template')) { done(document.getElementById('md_template').text); return; } var h = location.hash; if (done && h) { // #md/template/base_64_markdown var tk = h.split('/'); if (tk[0] === '#md') { done(atob(tk.slice(2).join('/')), tk[1]); } } } }; function Slide(tree, actions, prop) { var html; var md_tree = tree; var properties = prop; var action; function compile(context) { propertiesToActions(); //sort actions by order actions.sort(function(a, b) { return a.order - b.order; }); if (!('map' in Array.prototype)) { Array.prototype.map= function(mapper, that /*opt*/) { var other= new Array(this.length); for (var i= 0, n= this.length; i= offset) { var t = (offset - bounds.top)/(bounds.bottom - bounds.top); return t; } }); return r; }; scroll.less = function(el, opt) { opt = opt || {}; var r = scroll.reach(el); var fixedBoundsTop; if (opt.fixed) { var e = Core.getElement(el); fixedBoundsTop = scroller.scrollY + e.getBoundingClientRect().top; } r.condition(function(bounds, offset, scrollY) { var t = opt.fixed ? fixedBoundsTop: bounds.top; var o = opt.fixed ? scrollY: offset; if(t >= o) { return 0; } }); return r; }; scroll.greater = function(el, opt) { opt = opt || {}; var r = scroll.reach(el); var fixedBoundsTop; if (opt.fixed) { var e = Core.getElement(el); fixedBoundsTop = scroller.scrollY + e.getBoundingClientRect().top; } r.condition(function(bounds, offset, scrollY) { var t = opt.fixed ? fixedBoundsTop: bounds.top; var o = opt.fixed ? scrollY: offset; if(t <= o) { return 0; } }); return r; }; function register(s) { scrolls.push(s); initScroll(); } function unregister(s) { var i = scrolls.indexOf(s); if (i >= 0) { scrolls.splice(i, 1); } } function initScroll() { if (!initialized) { initialized = true; if (!Array.prototype.forEach) { Array.prototype.forEach = function (callback, thisArg) { var T, k; if (this == null) { throw new TypeError(" this is null or not defined"); } // 1. Let O be the result of calling ToObject passing the |this| value as the argument. var O = Object(this); // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". // 3. Let len be ToUint32(lenValue). var len = O.length >>> 0; // 4. If IsCallable(callback) is false, throw a TypeError exception. // See: http://es5.github.com/#x9.11 if (typeof callback !== "function") { throw new TypeError(callback + " is not a function"); } // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. if (thisArg) { T = thisArg; } // 6. Let k be 0 k = 0; // 7. Repeat, while k < len while (k < len) { var kValue; // a. Let Pk be ToString(k). // This is implicit for LHS operands of the in operator // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. // This step can be combined with c // c. If kPresent is true, then if (k in O) { // i. Let kValue be the result of calling the Get internal method of O with argument Pk. kValue = O[k]; // ii. Call the Call internal method of callback with T as the this value and // argument list containing kValue, k, and O. callback.call(T, kValue, k, O); } // d. Increase k by 1. k++; } // 8. return undefined }; } function scrollEach() { scrolls.forEach(function(s) { s.scroll(window.scrollY); }); } if (!window.addEventListener) { scroller.attachEvent("onscroll", function load(event) { scrollEach(); }); } else { window.addEventListener("scroll", function load(event) { scrollEach(); }); } } } return scroll; } Scroll._scrolls = []; module.exports = Scroll; },{"../core":13,"../story":14}],20:[function(_dereq_,module,exports){ var Trigger = _dereq_('../story').Trigger; function Sequential() { var current = 0; var steps = []; var max = 0; var triggers = {}; function seq() {} function update() { for (var i = 0; i < steps.length; ++i) { steps[i].check(); } } seq.state = seq.step = function(n) { if (n in triggers) { return triggers[n]; } var t = Trigger({ check: function() { if (n === current && this.trigger) this.trigger(); }, reverse: function() { current = n; } }); max = Math.max(max, n); steps.push(t); return triggers[n] = t; }; seq.next = function() { current += 1; if (current > max) { current = 0; } update(); }; seq.clear = function() { steps = []; max = 0; current = 0; return seq; }; seq.current = function(_) { if (_ !== undefined) { var c = Math.max(Math.min(max, _), 0); if (c !== current) { current = c; update(); } return this; } return current; }; seq.prev = function() { current -= 1; if (current < 0) { current = max; } update(); }; return seq; } module.exports = Sequential; },{"../story":14}],21:[function(_dereq_,module,exports){ function Video(player) { if (typeof YT === 'undefined' || !(player instanceof YT.Player)) { throw new Error("player should be a YT.Player instance, see youtube API"); } var triggers = []; var i = setInterval(function() { var seconds = player.getCurrentTime(); for (var i = 0; i < triggers.length; ++i) { var t = triggers[i]; if (t.start <= seconds && t.end > seconds) { t.trigger((seconds - t.start)/(t.end - t.start)); } } }, 100); var clear = function(t) { if (triggers.length === 0) { clearInterval(i); } }; return { between: function(start, end) { var t = O.Trigger(); t.start = start; t.end = end; triggers.push(t); t.clear = function() { triggers.splice(triggers.indexOf(t), 1); clear(); }; return t; } } } module.exports = Video },{}],22:[function(_dereq_,module,exports){ /** # dot progress ui widget that controls dot progress ## usage in order to use it you need to instanciate using a container, so for example:
... var progress = DotProgress('dots') // we set the number of slides progress.count(10); // we can activate it as an action // then the story enters in this state the second dot // will be activated story.addState(trigger, progress.activate(1)) // when an user clicks on the dot it can trigger an action story.addState(progress.step(1), action); ## styling the html rendered is the following: ```
``` so you can use active class to style the active one */ var Core = _dereq_('../core'); function DotProgress(el) { var count = 0; var element = Core.getElement(el); var triggers = {}; function _progress() { return _progress; } function render() { var html = '"; element.innerHTML = html; } _progress.count = function(_) { count = _; render(); return _progress; }; // returns an action to activate the index _progress.activate = function(activeIndex) { return O.Action(function () { var children = element.children[0].children; for(var i = 0; i < children.length; ++i) { children[i].children[0].setAttribute('class', ''); } children[activeIndex].children[0].setAttribute('class', 'active'); }); }; element.onclick = function(e) { e = e || window.event; var idx = (e.target || e.srcElement).getAttribute('slide-index'); var t = triggers[idx]; if (t) { t.trigger(); } }; _progress.step = function(i) { var t = O.Trigger(); triggers[i] = t; return t; }; return _progress; } module.exports = DotProgress; },{"../core":13}],23:[function(_dereq_,module,exports){ module.exports = { DotProgress: _dereq_('./dotprogress') } },{"./dotprogress":22}],24:[function(_dereq_,module,exports){ /* * classList.js: Cross-browser full element.classList implementation. * 2014-01-31 * * By Eli Grey, http://eligrey.com * Public Domain. * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. */ /*global self, document, DOMException */ /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/ if ("document" in self && !("classList" in document.createElement("_"))) { (function (view) { "use strict"; if (!('Element' in view)) return; var classListProp = "classList" , protoProp = "prototype" , elemCtrProto = view.Element[protoProp] , objCtr = Object , strTrim = String[protoProp].trim || function () { return this.replace(/^\s+|\s+$/g, ""); } , arrIndexOf = Array[protoProp].indexOf || function (item) { var i = 0 , len = this.length ; for (; i < len; i++) { if (i in this && this[i] === item) { return i; } } return -1; } // Vendors: please allow content code to instantiate DOMExceptions , DOMEx = function (type, message) { this.name = type; this.code = DOMException[type]; this.message = message; } , checkTokenAndGetIndex = function (classList, token) { if (token === "") { throw new DOMEx( "SYNTAX_ERR" , "An invalid or illegal string was specified" ); } if (/\s/.test(token)) { throw new DOMEx( "INVALID_CHARACTER_ERR" , "String contains an invalid character" ); } return arrIndexOf.call(classList, token); } , ClassList = function (elem) { var trimmedClasses = strTrim.call(elem.getAttribute("class") || "") , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [] , i = 0 , len = classes.length ; for (; i < len; i++) { this.push(classes[i]); } this._updateClassName = function () { elem.setAttribute("class", this.toString()); }; } , classListProto = ClassList[protoProp] = [] , classListGetter = function () { return new ClassList(this); } ; // Most DOMException implementations don't allow calling DOMException's toString() // on non-DOMExceptions. Error's toString() is sufficient here. DOMEx[protoProp] = Error[protoProp]; classListProto.item = function (i) { return this[i] || null; }; classListProto.contains = function (token) { token += ""; return checkTokenAndGetIndex(this, token) !== -1; }; classListProto.add = function () { var tokens = arguments , i = 0 , l = tokens.length , token , updated = false ; do { token = tokens[i] + ""; if (checkTokenAndGetIndex(this, token) === -1) { this.push(token); updated = true; } } while (++i < l); if (updated) { this._updateClassName(); } }; classListProto.remove = function () { var tokens = arguments , i = 0 , l = tokens.length , token , updated = false ; do { token = tokens[i] + ""; var index = checkTokenAndGetIndex(this, token); if (index !== -1) { this.splice(index, 1); updated = true; } } while (++i < l); if (updated) { this._updateClassName(); } }; classListProto.toggle = function (token, force) { token += ""; var result = this.contains(token) , method = result ? force !== true && "remove" : force !== false && "add" ; if (method) { this[method](token); } return !result; }; classListProto.toString = function () { return this.join(" "); }; if (objCtr.defineProperty) { var classListPropDesc = { get: classListGetter , enumerable: true , configurable: true }; try { objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); } catch (ex) { // IE 8 doesn't support enumerable:true if (ex.number === -0x7FF5EC54) { classListPropDesc.enumerable = false; objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); } } } else if (objCtr[protoProp].__defineGetter__) { elemCtrProto.__defineGetter__(classListProp, classListGetter); } }(self)); } },{}],25:[function(_dereq_,module,exports){ /** * new L.CrossHair('http://image.com/image', { x: 10, y :10 }).addTo(map) */ if (typeof window.L !== 'undefined') { L.CrossHair = L.Control.extend({ initialize: function(img, size) { this.image = new Image(); this.image.onload = L.bind(this._updatePos, this); this.image.src = img; if (size) { this.image.width = size.x; this.image.height = size.y; } }, _updatePos: function() { if (this._map) { var w = this.image.width >> 1; var h = this.image.height >> 1; w = this._map.getSize().x/2.0 - w; h = this._map.getSize().y/2.0 - h; L.DomUtil.setPosition(this.image, { x: w, y: h }); } }, addTo: function(map) { var r = L.Control.prototype.addTo.call(this, map); // remove leaflet-top and leaflet-right classes this.image.parentNode.setAttribute('class', ''); map.on('resize', L.bind(this._updatePos, this)); return r; }, onRemove: function(map) { map.off('resize', null, this); }, onAdd: function(map) { this._updatePos(); return this.image; } }); } },{}],26:[function(_dereq_,module,exports){ module.exports = { CrossHair: _dereq_('./crosshair') } },{"./crosshair":25}],27:[function(_dereq_,module,exports){ if (typeof Object.create === 'function') { // implementation from standard node.js 'util' module module.exports = function inherits(ctor, superCtor) { ctor.super_ = superCtor ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); }; } else { // old school shim for old browsers module.exports = function inherits(ctor, superCtor) { ctor.super_ = superCtor var TempCtor = function () {} TempCtor.prototype = superCtor.prototype ctor.prototype = new TempCtor() ctor.prototype.constructor = ctor } } },{}],28:[function(_dereq_,module,exports){ // shim for using process in browser var process = module.exports = {}; process.nextTick = (function () { var canSetImmediate = typeof window !== 'undefined' && window.setImmediate; var canPost = typeof window !== 'undefined' && window.postMessage && window.addEventListener ; if (canSetImmediate) { return function (f) { return window.setImmediate(f) }; } if (canPost) { var queue = []; window.addEventListener('message', function (ev) { var source = ev.source; if ((source === window || source === null) && ev.data === 'process-tick') { ev.stopPropagation(); if (queue.length > 0) { var fn = queue.shift(); fn(); } } }, true); return function nextTick(fn) { queue.push(fn); window.postMessage('process-tick', '*'); }; } return function nextTick(fn) { setTimeout(fn, 0); }; })(); process.title = 'browser'; process.browser = true; process.env = {}; process.argv = []; function noop() {} process.on = noop; process.addListener = noop; process.once = noop; process.off = noop; process.removeListener = noop; process.removeAllListeners = noop; process.emit = noop; process.binding = function (name) { throw new Error('process.binding is not supported'); } // TODO(shtylman) process.cwd = function () { return '/' }; process.chdir = function (dir) { throw new Error('process.chdir is not supported'); }; },{}],29:[function(_dereq_,module,exports){ module.exports = function isBuffer(arg) { return arg && typeof arg === 'object' && typeof arg.copy === 'function' && typeof arg.fill === 'function' && typeof arg.readUInt8 === 'function'; } },{}],30:[function(_dereq_,module,exports){ (function (process,global){ // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // 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. var formatRegExp = /%[sdj%]/g; exports.format = function(f) { if (!isString(f)) { var objects = []; for (var i = 0; i < arguments.length; i++) { objects.push(inspect(arguments[i])); } return objects.join(' '); } var i = 1; var args = arguments; var len = args.length; var str = String(f).replace(formatRegExp, function(x) { if (x === '%%') return '%'; if (i >= len) return x; switch (x) { case '%s': return String(args[i++]); case '%d': return Number(args[i++]); case '%j': try { return JSON.stringify(args[i++]); } catch (_) { return '[Circular]'; } default: return x; } }); for (var x = args[i]; i < len; x = args[++i]) { if (isNull(x) || !isObject(x)) { str += ' ' + x; } else { str += ' ' + inspect(x); } } return str; }; // Mark that a method should not be used. // Returns a modified function which warns once by default. // If --no-deprecation is set, then it is a no-op. exports.deprecate = function(fn, msg) { // Allow for deprecating things in the process of starting up. if (isUndefined(global.process)) { return function() { return exports.deprecate(fn, msg).apply(this, arguments); }; } if (process.noDeprecation === true) { return fn; } var warned = false; function deprecated() { if (!warned) { if (process.throwDeprecation) { throw new Error(msg); } else if (process.traceDeprecation) { console.trace(msg); } else { console.error(msg); } warned = true; } return fn.apply(this, arguments); } return deprecated; }; var debugs = {}; var debugEnviron; exports.debuglog = function(set) { if (isUndefined(debugEnviron)) debugEnviron = process.env.NODE_DEBUG || ''; set = set.toUpperCase(); if (!debugs[set]) { if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) { var pid = process.pid; debugs[set] = function() { var msg = exports.format.apply(exports, arguments); console.error('%s %d: %s', set, pid, msg); }; } else { debugs[set] = function() {}; } } return debugs[set]; }; /** * Echos the value of a value. Trys to print the value out * in the best way possible given the different types. * * @param {Object} obj The object to print out. * @param {Object} opts Optional options object that alters the output. */ /* legacy: obj, showHidden, depth, colors*/ function inspect(obj, opts) { // default options var ctx = { seen: [], stylize: stylizeNoColor }; // legacy... if (arguments.length >= 3) ctx.depth = arguments[2]; if (arguments.length >= 4) ctx.colors = arguments[3]; if (isBoolean(opts)) { // legacy... ctx.showHidden = opts; } else if (opts) { // got an "options" object exports._extend(ctx, opts); } // set default options if (isUndefined(ctx.showHidden)) ctx.showHidden = false; if (isUndefined(ctx.depth)) ctx.depth = 2; if (isUndefined(ctx.colors)) ctx.colors = false; if (isUndefined(ctx.customInspect)) ctx.customInspect = true; if (ctx.colors) ctx.stylize = stylizeWithColor; return formatValue(ctx, obj, ctx.depth); } exports.inspect = inspect; // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics inspect.colors = { 'bold' : [1, 22], 'italic' : [3, 23], 'underline' : [4, 24], 'inverse' : [7, 27], 'white' : [37, 39], 'grey' : [90, 39], 'black' : [30, 39], 'blue' : [34, 39], 'cyan' : [36, 39], 'green' : [32, 39], 'magenta' : [35, 39], 'red' : [31, 39], 'yellow' : [33, 39] }; // Don't use 'blue' not visible on cmd.exe inspect.styles = { 'special': 'cyan', 'number': 'yellow', 'boolean': 'yellow', 'undefined': 'grey', 'null': 'bold', 'string': 'green', 'date': 'magenta', // "name": intentionally not styling 'regexp': 'red' }; function stylizeWithColor(str, styleType) { var style = inspect.styles[styleType]; if (style) { return '\u001b[' + inspect.colors[style][0] + 'm' + str + '\u001b[' + inspect.colors[style][1] + 'm'; } else { return str; } } function stylizeNoColor(str, styleType) { return str; } function arrayToHash(array) { var hash = {}; array.forEach(function(val, idx) { hash[val] = true; }); return hash; } function formatValue(ctx, value, recurseTimes) { // Provide a hook for user-specified inspect functions. // Check that value is an object with an inspect function on it if (ctx.customInspect && value && isFunction(value.inspect) && // Filter out the util module, it's inspect function is special value.inspect !== exports.inspect && // Also filter out any prototype objects using the circular check. !(value.constructor && value.constructor.prototype === value)) { var ret = value.inspect(recurseTimes, ctx); if (!isString(ret)) { ret = formatValue(ctx, ret, recurseTimes); } return ret; } // Primitive types cannot have properties var primitive = formatPrimitive(ctx, value); if (primitive) { return primitive; } // Look up the keys of the object. var keys = Object.keys(value); var visibleKeys = arrayToHash(keys); if (ctx.showHidden) { keys = Object.getOwnPropertyNames(value); } // IE doesn't make error fields non-enumerable // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx if (isError(value) && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) { return formatError(value); } // Some type of object without properties can be shortcutted. if (keys.length === 0) { if (isFunction(value)) { var name = value.name ? ': ' + value.name : ''; return ctx.stylize('[Function' + name + ']', 'special'); } if (isRegExp(value)) { return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); } if (isDate(value)) { return ctx.stylize(Date.prototype.toString.call(value), 'date'); } if (isError(value)) { return formatError(value); } } var base = '', array = false, braces = ['{', '}']; // Make Array say that they are Array if (isArray(value)) { array = true; braces = ['[', ']']; } // Make functions say that they are functions if (isFunction(value)) { var n = value.name ? ': ' + value.name : ''; base = ' [Function' + n + ']'; } // Make RegExps say that they are RegExps if (isRegExp(value)) { base = ' ' + RegExp.prototype.toString.call(value); } // Make dates with properties first say the date if (isDate(value)) { base = ' ' + Date.prototype.toUTCString.call(value); } // Make error with message first say the error if (isError(value)) { base = ' ' + formatError(value); } if (keys.length === 0 && (!array || value.length == 0)) { return braces[0] + base + braces[1]; } if (recurseTimes < 0) { if (isRegExp(value)) { return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); } else { return ctx.stylize('[Object]', 'special'); } } ctx.seen.push(value); var output; if (array) { output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); } else { output = keys.map(function(key) { return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); }); } ctx.seen.pop(); return reduceToSingleString(output, base, braces); } function formatPrimitive(ctx, value) { if (isUndefined(value)) return ctx.stylize('undefined', 'undefined'); if (isString(value)) { var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') .replace(/'/g, "\\'") .replace(/\\"/g, '"') + '\''; return ctx.stylize(simple, 'string'); } if (isNumber(value)) return ctx.stylize('' + value, 'number'); if (isBoolean(value)) return ctx.stylize('' + value, 'boolean'); // For some reason typeof null is "object", so special case here. if (isNull(value)) return ctx.stylize('null', 'null'); } function formatError(value) { return '[' + Error.prototype.toString.call(value) + ']'; } function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { var output = []; for (var i = 0, l = value.length; i < l; ++i) { if (hasOwnProperty(value, String(i))) { output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, String(i), true)); } else { output.push(''); } } keys.forEach(function(key) { if (!key.match(/^\d+$/)) { output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, key, true)); } }); return output; } function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { var name, str, desc; desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; if (desc.get) { if (desc.set) { str = ctx.stylize('[Getter/Setter]', 'special'); } else { str = ctx.stylize('[Getter]', 'special'); } } else { if (desc.set) { str = ctx.stylize('[Setter]', 'special'); } } if (!hasOwnProperty(visibleKeys, key)) { name = '[' + key + ']'; } if (!str) { if (ctx.seen.indexOf(desc.value) < 0) { if (isNull(recurseTimes)) { str = formatValue(ctx, desc.value, null); } else { str = formatValue(ctx, desc.value, recurseTimes - 1); } if (str.indexOf('\n') > -1) { if (array) { str = str.split('\n').map(function(line) { return ' ' + line; }).join('\n').substr(2); } else { str = '\n' + str.split('\n').map(function(line) { return ' ' + line; }).join('\n'); } } } else { str = ctx.stylize('[Circular]', 'special'); } } if (isUndefined(name)) { if (array && key.match(/^\d+$/)) { return str; } name = JSON.stringify('' + key); if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { name = name.substr(1, name.length - 2); name = ctx.stylize(name, 'name'); } else { name = name.replace(/'/g, "\\'") .replace(/\\"/g, '"') .replace(/(^"|"$)/g, "'"); name = ctx.stylize(name, 'string'); } } return name + ': ' + str; } function reduceToSingleString(output, base, braces) { var numLinesEst = 0; var length = output.reduce(function(prev, cur) { numLinesEst++; if (cur.indexOf('\n') >= 0) numLinesEst++; return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; }, 0); if (length > 60) { return braces[0] + (base === '' ? '' : base + '\n ') + ' ' + output.join(',\n ') + ' ' + braces[1]; } return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; } // NOTE: These type checking functions intentionally don't use `instanceof` // because it is fragile and can be easily faked with `Object.create()`. function isArray(ar) { return Array.isArray(ar); } exports.isArray = isArray; function isBoolean(arg) { return typeof arg === 'boolean'; } exports.isBoolean = isBoolean; function isNull(arg) { return arg === null; } exports.isNull = isNull; function isNullOrUndefined(arg) { return arg == null; } exports.isNullOrUndefined = isNullOrUndefined; function isNumber(arg) { return typeof arg === 'number'; } exports.isNumber = isNumber; function isString(arg) { return typeof arg === 'string'; } exports.isString = isString; function isSymbol(arg) { return typeof arg === 'symbol'; } exports.isSymbol = isSymbol; function isUndefined(arg) { return arg === void 0; } exports.isUndefined = isUndefined; function isRegExp(re) { return isObject(re) && objectToString(re) === '[object RegExp]'; } exports.isRegExp = isRegExp; function isObject(arg) { return typeof arg === 'object' && arg !== null; } exports.isObject = isObject; function isDate(d) { return isObject(d) && objectToString(d) === '[object Date]'; } exports.isDate = isDate; function isError(e) { return isObject(e) && (objectToString(e) === '[object Error]' || e instanceof Error); } exports.isError = isError; function isFunction(arg) { return typeof arg === 'function'; } exports.isFunction = isFunction; function isPrimitive(arg) { return arg === null || typeof arg === 'boolean' || typeof arg === 'number' || typeof arg === 'string' || typeof arg === 'symbol' || // ES6 symbol typeof arg === 'undefined'; } exports.isPrimitive = isPrimitive; exports.isBuffer = _dereq_('./support/isBuffer'); function objectToString(o) { return Object.prototype.toString.call(o); } function pad(n) { return n < 10 ? '0' + n.toString(10) : n.toString(10); } var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // 26 Feb 16:19:34 function timestamp() { var d = new Date(); var time = [pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds())].join(':'); return [d.getDate(), months[d.getMonth()], time].join(' '); } // log is just a thin wrapper to console.log that prepends a timestamp exports.log = function() { console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); }; /** * Inherit the prototype methods from one constructor into another. * * The Function.prototype.inherits from lang.js rewritten as a standalone * function (not on Function.prototype). NOTE: If this file is to be loaded * during bootstrapping this function needs to be rewritten using some native * functions as prototype setup using normal JavaScript does not work as * expected during bootstrapping (see mirror.js in r114903). * * @param {function} ctor Constructor function which needs to inherit the * prototype. * @param {function} superCtor Constructor function to inherit prototype from. */ exports.inherits = _dereq_('inherits'); exports._extend = function(origin, add) { // Don't do anything if add isn't an object if (!add || !isObject(add)) return origin; var keys = Object.keys(add); var i = keys.length; while (i--) { origin[keys[i]] = add[keys[i]]; } return origin; }; function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } }).call(this,_dereq_("FWaASH"),typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./support/isBuffer":29,"FWaASH":28,"inherits":27}],31:[function(_dereq_,module,exports){ /*! Hammer.JS - v1.1.3 - 2014-05-20 * http://eightmedia.github.io/hammer.js * * Copyright (c) 2014 Jorik Tangelder ; * Licensed under the MIT license */ (function(window, undefined) { 'use strict'; /** * @main * @module hammer * * @class Hammer * @static */ /** * Hammer, use this to create instances * ```` * var hammertime = new Hammer(myElement); * ```` * * @method Hammer * @param {HTMLElement} element * @param {Object} [options={}] * @return {Hammer.Instance} */ var Hammer = function Hammer(element, options) { return new Hammer.Instance(element, options || {}); }; /** * version, as defined in package.json * the value will be set at each build * @property VERSION * @final * @type {String} */ Hammer.VERSION = '1.1.3'; /** * default settings. * more settings are defined per gesture at `/gestures`. Each gesture can be disabled/enabled * by setting it's name (like `swipe`) to false. * You can set the defaults for all instances by changing this object before creating an instance. * @example * ```` * Hammer.defaults.drag = false; * Hammer.defaults.behavior.touchAction = 'pan-y'; * delete Hammer.defaults.behavior.userSelect; * ```` * @property defaults * @type {Object} */ Hammer.defaults = { /** * this setting object adds styles and attributes to the element to prevent the browser from doing * its native behavior. The css properties are auto prefixed for the browsers when needed. * @property defaults.behavior * @type {Object} */ behavior: { /** * Disables text selection to improve the dragging gesture. When the value is `none` it also sets * `onselectstart=false` for IE on the element. Mainly for desktop browsers. * @property defaults.behavior.userSelect * @type {String} * @default 'none' */ userSelect: 'none', /** * Specifies whether and how a given region can be manipulated by the user (for instance, by panning or zooming). * Used by Chrome 35> and IE10>. By default this makes the element blocking any touch event. * @property defaults.behavior.touchAction * @type {String} * @default: 'pan-y' */ touchAction: 'pan-y', /** * Disables the default callout shown when you touch and hold a touch target. * On iOS, when you touch and hold a touch target such as a link, Safari displays * a callout containing information about the link. This property allows you to disable that callout. * @property defaults.behavior.touchCallout * @type {String} * @default 'none' */ touchCallout: 'none', /** * Specifies whether zooming is enabled. Used by IE10> * @property defaults.behavior.contentZooming * @type {String} * @default 'none' */ contentZooming: 'none', /** * Specifies that an entire element should be draggable instead of its contents. * Mainly for desktop browsers. * @property defaults.behavior.userDrag * @type {String} * @default 'none' */ userDrag: 'none', /** * Overrides the highlight color shown when the user taps a link or a JavaScript * clickable element in Safari on iPhone. This property obeys the alpha value, if specified. * * If you don't specify an alpha value, Safari on iPhone applies a default alpha value * to the color. To disable tap highlighting, set the alpha value to 0 (invisible). * If you set the alpha value to 1.0 (opaque), the element is not visible when tapped. * @property defaults.behavior.tapHighlightColor * @type {String} * @default 'rgba(0,0,0,0)' */ tapHighlightColor: 'rgba(0,0,0,0)' } }; /** * hammer document where the base events are added at * @property DOCUMENT * @type {HTMLElement} * @default window.document */ Hammer.DOCUMENT = document; /** * detect support for pointer events * @property HAS_POINTEREVENTS * @type {Boolean} */ Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; /** * detect support for touch events * @property HAS_TOUCHEVENTS * @type {Boolean} */ Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); /** * detect mobile browsers * @property IS_MOBILE * @type {Boolean} */ Hammer.IS_MOBILE = /mobile|tablet|ip(ad|hone|od)|android|silk/i.test(navigator.userAgent); /** * detect if we want to support mouseevents at all * @property NO_MOUSEEVENTS * @type {Boolean} */ Hammer.NO_MOUSEEVENTS = (Hammer.HAS_TOUCHEVENTS && Hammer.IS_MOBILE) || Hammer.HAS_POINTEREVENTS; /** * interval in which Hammer recalculates current velocity/direction/angle in ms * @property CALCULATE_INTERVAL * @type {Number} * @default 25 */ Hammer.CALCULATE_INTERVAL = 25; /** * eventtypes per touchevent (start, move, end) are filled by `Event.determineEventTypes` on `setup` * the object contains the DOM event names per type (`EVENT_START`, `EVENT_MOVE`, `EVENT_END`) * @property EVENT_TYPES * @private * @writeOnce * @type {Object} */ var EVENT_TYPES = {}; /** * direction strings, for safe comparisons * @property DIRECTION_DOWN|LEFT|UP|RIGHT * @final * @type {String} * @default 'down' 'left' 'up' 'right' */ var DIRECTION_DOWN = Hammer.DIRECTION_DOWN = 'down'; var DIRECTION_LEFT = Hammer.DIRECTION_LEFT = 'left'; var DIRECTION_UP = Hammer.DIRECTION_UP = 'up'; var DIRECTION_RIGHT = Hammer.DIRECTION_RIGHT = 'right'; /** * pointertype strings, for safe comparisons * @property POINTER_MOUSE|TOUCH|PEN * @final * @type {String} * @default 'mouse' 'touch' 'pen' */ var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse'; var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch'; var POINTER_PEN = Hammer.POINTER_PEN = 'pen'; /** * eventtypes * @property EVENT_START|MOVE|END|RELEASE|TOUCH * @final * @type {String} * @default 'start' 'change' 'move' 'end' 'release' 'touch' */ var EVENT_START = Hammer.EVENT_START = 'start'; var EVENT_MOVE = Hammer.EVENT_MOVE = 'move'; var EVENT_END = Hammer.EVENT_END = 'end'; var EVENT_RELEASE = Hammer.EVENT_RELEASE = 'release'; var EVENT_TOUCH = Hammer.EVENT_TOUCH = 'touch'; /** * if the window events are set... * @property READY * @writeOnce * @type {Boolean} * @default false */ Hammer.READY = false; /** * plugins namespace * @property plugins * @type {Object} */ Hammer.plugins = Hammer.plugins || {}; /** * gestures namespace * see `/gestures` for the definitions * @property gestures * @type {Object} */ Hammer.gestures = Hammer.gestures || {}; /** * setup events to detect gestures on the document * this function is called when creating an new instance * @private */ function setup() { if(Hammer.READY) { return; } // find what eventtypes we add listeners to Event.determineEventTypes(); // Register all gestures inside Hammer.gestures Utils.each(Hammer.gestures, function(gesture) { Detection.register(gesture); }); // Add touch events on the document Event.onTouch(Hammer.DOCUMENT, EVENT_MOVE, Detection.detect); Event.onTouch(Hammer.DOCUMENT, EVENT_END, Detection.detect); // Hammer is ready...! Hammer.READY = true; } /** * @module hammer * * @class Utils * @static */ var Utils = Hammer.utils = { /** * extend method, could also be used for cloning when `dest` is an empty object. * changes the dest object * @method extend * @param {Object} dest * @param {Object} src * @param {Boolean} [merge=false] do a merge * @return {Object} dest */ extend: function extend(dest, src, merge) { for(var key in src) { if(!src.hasOwnProperty(key) || (dest[key] !== undefined && merge)) { continue; } dest[key] = src[key]; } return dest; }, /** * simple addEventListener wrapper * @method on * @param {HTMLElement} element * @param {String} type * @param {Function} handler */ on: function on(element, type, handler) { element.addEventListener(type, handler, false); }, /** * simple removeEventListener wrapper * @method off * @param {HTMLElement} element * @param {String} type * @param {Function} handler */ off: function off(element, type, handler) { element.removeEventListener(type, handler, false); }, /** * forEach over arrays and objects * @method each * @param {Object|Array} obj * @param {Function} iterator * @param {any} iterator.item * @param {Number} iterator.index * @param {Object|Array} iterator.obj the source object * @param {Object} context value to use as `this` in the iterator */ each: function each(obj, iterator, context) { var i, len; // native forEach on arrays if('forEach' in obj) { obj.forEach(iterator, context); // arrays } else if(obj.length !== undefined) { for(i = 0, len = obj.length; i < len; i++) { if(iterator.call(context, obj[i], i, obj) === false) { return; } } // objects } else { for(i in obj) { if(obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj) === false) { return; } } } }, /** * find if a string contains the string using indexOf * @method inStr * @param {String} src * @param {String} find * @return {Boolean} found */ inStr: function inStr(src, find) { return src.indexOf(find) > -1; }, /** * find if a array contains the object using indexOf or a simple polyfill * @method inArray * @param {String} src * @param {String} find * @return {Boolean|Number} false when not found, or the index */ inArray: function inArray(src, find) { if(src.indexOf) { var index = src.indexOf(find); return (index === -1) ? false : index; } else { for(var i = 0, len = src.length; i < len; i++) { if(src[i] === find) { return i; } } return false; } }, /** * convert an array-like object (`arguments`, `touchlist`) to an array * @method toArray * @param {Object} obj * @return {Array} */ toArray: function toArray(obj) { return Array.prototype.slice.call(obj, 0); }, /** * find if a node is in the given parent * @method hasParent * @param {HTMLElement} node * @param {HTMLElement} parent * @return {Boolean} found */ hasParent: function hasParent(node, parent) { while(node) { if(node == parent) { return true; } node = node.parentNode; } return false; }, /** * get the center of all the touches * @method getCenter * @param {Array} touches * @return {Object} center contains `pageX`, `pageY`, `clientX` and `clientY` properties */ getCenter: function getCenter(touches) { var pageX = [], pageY = [], clientX = [], clientY = [], min = Math.min, max = Math.max; // no need to loop when only one touch if(touches.length === 1) { return { pageX: touches[0].pageX, pageY: touches[0].pageY, clientX: touches[0].clientX, clientY: touches[0].clientY }; } Utils.each(touches, function(touch) { pageX.push(touch.pageX); pageY.push(touch.pageY); clientX.push(touch.clientX); clientY.push(touch.clientY); }); return { pageX: (min.apply(Math, pageX) + max.apply(Math, pageX)) / 2, pageY: (min.apply(Math, pageY) + max.apply(Math, pageY)) / 2, clientX: (min.apply(Math, clientX) + max.apply(Math, clientX)) / 2, clientY: (min.apply(Math, clientY) + max.apply(Math, clientY)) / 2 }; }, /** * calculate the velocity between two points. unit is in px per ms. * @method getVelocity * @param {Number} deltaTime * @param {Number} deltaX * @param {Number} deltaY * @return {Object} velocity `x` and `y` */ getVelocity: function getVelocity(deltaTime, deltaX, deltaY) { return { x: Math.abs(deltaX / deltaTime) || 0, y: Math.abs(deltaY / deltaTime) || 0 }; }, /** * calculate the angle between two coordinates * @method getAngle * @param {Touch} touch1 * @param {Touch} touch2 * @return {Number} angle */ getAngle: function getAngle(touch1, touch2) { var x = touch2.clientX - touch1.clientX, y = touch2.clientY - touch1.clientY; return Math.atan2(y, x) * 180 / Math.PI; }, /** * do a small comparision to get the direction between two touches. * @method getDirection * @param {Touch} touch1 * @param {Touch} touch2 * @return {String} direction matches `DIRECTION_LEFT|RIGHT|UP|DOWN` */ getDirection: function getDirection(touch1, touch2) { var x = Math.abs(touch1.clientX - touch2.clientX), y = Math.abs(touch1.clientY - touch2.clientY); if(x >= y) { return touch1.clientX - touch2.clientX > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; } return touch1.clientY - touch2.clientY > 0 ? DIRECTION_UP : DIRECTION_DOWN; }, /** * calculate the distance between two touches * @method getDistance * @param {Touch}touch1 * @param {Touch} touch2 * @return {Number} distance */ getDistance: function getDistance(touch1, touch2) { var x = touch2.clientX - touch1.clientX, y = touch2.clientY - touch1.clientY; return Math.sqrt((x * x) + (y * y)); }, /** * calculate the scale factor between two touchLists * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out * @method getScale * @param {Array} start array of touches * @param {Array} end array of touches * @return {Number} scale */ getScale: function getScale(start, end) { // need two fingers... if(start.length >= 2 && end.length >= 2) { return this.getDistance(end[0], end[1]) / this.getDistance(start[0], start[1]); } return 1; }, /** * calculate the rotation degrees between two touchLists * @method getRotation * @param {Array} start array of touches * @param {Array} end array of touches * @return {Number} rotation */ getRotation: function getRotation(start, end) { // need two fingers if(start.length >= 2 && end.length >= 2) { return this.getAngle(end[1], end[0]) - this.getAngle(start[1], start[0]); } return 0; }, /** * find out if the direction is vertical * * @method isVertical * @param {String} direction matches `DIRECTION_UP|DOWN` * @return {Boolean} is_vertical */ isVertical: function isVertical(direction) { return direction == DIRECTION_UP || direction == DIRECTION_DOWN; }, /** * set css properties with their prefixes * @param {HTMLElement} element * @param {String} prop * @param {String} value * @param {Boolean} [toggle=true] * @return {Boolean} */ setPrefixedCss: function setPrefixedCss(element, prop, value, toggle) { var prefixes = ['', 'Webkit', 'Moz', 'O', 'ms']; prop = Utils.toCamelCase(prop); for(var i = 0; i < prefixes.length; i++) { var p = prop; // prefixes if(prefixes[i]) { p = prefixes[i] + p.slice(0, 1).toUpperCase() + p.slice(1); } // test the style if(p in element.style) { element.style[p] = (toggle == null || toggle) && value || ''; break; } } }, /** * toggle browser default behavior by setting css properties. * `userSelect='none'` also sets `element.onselectstart` to false * `userDrag='none'` also sets `element.ondragstart` to false * * @method toggleBehavior * @param {HtmlElement} element * @param {Object} props * @param {Boolean} [toggle=true] */ toggleBehavior: function toggleBehavior(element, props, toggle) { if(!props || !element || !element.style) { return; } // set the css properties Utils.each(props, function(value, prop) { Utils.setPrefixedCss(element, prop, value, toggle); }); var falseFn = toggle && function() { return false; }; // also the disable onselectstart if(props.userSelect == 'none') { element.onselectstart = falseFn; } // and disable ondragstart if(props.userDrag == 'none') { element.ondragstart = falseFn; } }, /** * convert a string with underscores to camelCase * so prevent_default becomes preventDefault * @param {String} str * @return {String} camelCaseStr */ toCamelCase: function toCamelCase(str) { return str.replace(/[_-]([a-z])/g, function(s) { return s[1].toUpperCase(); }); } }; /** * @module hammer */ /** * @class Event * @static */ var Event = Hammer.event = { /** * when touch events have been fired, this is true * this is used to stop mouse events * @property prevent_mouseevents * @private * @type {Boolean} */ preventMouseEvents: false, /** * if EVENT_START has been fired * @property started * @private * @type {Boolean} */ started: false, /** * when the mouse is hold down, this is true * @property should_detect * @private * @type {Boolean} */ shouldDetect: false, /** * simple event binder with a hook and support for multiple types * @method on * @param {HTMLElement} element * @param {String} type * @param {Function} handler * @param {Function} [hook] * @param {Object} hook.type */ on: function on(element, type, handler, hook) { var types = type.split(' '); Utils.each(types, function(type) { Utils.on(element, type, handler); hook && hook(type); }); }, /** * simple event unbinder with a hook and support for multiple types * @method off * @param {HTMLElement} element * @param {String} type * @param {Function} handler * @param {Function} [hook] * @param {Object} hook.type */ off: function off(element, type, handler, hook) { var types = type.split(' '); Utils.each(types, function(type) { Utils.off(element, type, handler); hook && hook(type); }); }, /** * the core touch event handler. * this finds out if we should to detect gestures * @method onTouch * @param {HTMLElement} element * @param {String} eventType matches `EVENT_START|MOVE|END` * @param {Function} handler * @return onTouchHandler {Function} the core event handler */ onTouch: function onTouch(element, eventType, handler) { var self = this; var onTouchHandler = function onTouchHandler(ev) { var srcType = ev.type.toLowerCase(), isPointer = Hammer.HAS_POINTEREVENTS, isMouse = Utils.inStr(srcType, 'mouse'), triggerType; // if we are in a mouseevent, but there has been a touchevent triggered in this session // we want to do nothing. simply break out of the event. if(isMouse && self.preventMouseEvents) { return; // mousebutton must be down } else if(isMouse && eventType == EVENT_START && ev.button === 0) { self.preventMouseEvents = false; self.shouldDetect = true; } else if(isPointer && eventType == EVENT_START) { self.shouldDetect = (ev.buttons === 1 || PointerEvent.matchType(POINTER_TOUCH, ev)); // just a valid start event, but no mouse } else if(!isMouse && eventType == EVENT_START) { self.preventMouseEvents = true; self.shouldDetect = true; } // update the pointer event before entering the detection if(isPointer && eventType != EVENT_END) { PointerEvent.updatePointer(eventType, ev); } // we are in a touch/down state, so allowed detection of gestures if(self.shouldDetect) { triggerType = self.doDetect.call(self, ev, eventType, element, handler); } // ...and we are done with the detection // so reset everything to start each detection totally fresh if(triggerType == EVENT_END) { self.preventMouseEvents = false; self.shouldDetect = false; PointerEvent.reset(); // update the pointerevent object after the detection } if(isPointer && eventType == EVENT_END) { PointerEvent.updatePointer(eventType, ev); } }; this.on(element, EVENT_TYPES[eventType], onTouchHandler); return onTouchHandler; }, /** * the core detection method * this finds out what hammer-touch-events to trigger * @method doDetect * @param {Object} ev * @param {String} eventType matches `EVENT_START|MOVE|END` * @param {HTMLElement} element * @param {Function} handler * @return {String} triggerType matches `EVENT_START|MOVE|END` */ doDetect: function doDetect(ev, eventType, element, handler) { var touchList = this.getTouchList(ev, eventType); var touchListLength = touchList.length; var triggerType = eventType; var triggerChange = touchList.trigger; // used by fakeMultitouch plugin var changedLength = touchListLength; // at each touchstart-like event we want also want to trigger a TOUCH event... if(eventType == EVENT_START) { triggerChange = EVENT_TOUCH; // ...the same for a touchend-like event } else if(eventType == EVENT_END) { triggerChange = EVENT_RELEASE; // keep track of how many touches have been removed changedLength = touchList.length - ((ev.changedTouches) ? ev.changedTouches.length : 1); } // after there are still touches on the screen, // we just want to trigger a MOVE event. so change the START or END to a MOVE // but only after detection has been started, the first time we actualy want a START if(changedLength > 0 && this.started) { triggerType = EVENT_MOVE; } // detection has been started, we keep track of this, see above this.started = true; // generate some event data, some basic information var evData = this.collectEventData(element, triggerType, touchList, ev); // trigger the triggerType event before the change (TOUCH, RELEASE) events // but the END event should be at last if(eventType != EVENT_END) { handler.call(Detection, evData); } // trigger a change (TOUCH, RELEASE) event, this means the length of the touches changed if(triggerChange) { evData.changedLength = changedLength; evData.eventType = triggerChange; handler.call(Detection, evData); evData.eventType = triggerType; delete evData.changedLength; } // trigger the END event if(triggerType == EVENT_END) { handler.call(Detection, evData); // ...and we are done with the detection // so reset everything to start each detection totally fresh this.started = false; } return triggerType; }, /** * we have different events for each device/browser * determine what we need and set them in the EVENT_TYPES constant * the `onTouch` method is bind to these properties. * @method determineEventTypes * @return {Object} events */ determineEventTypes: function determineEventTypes() { var types; if(Hammer.HAS_POINTEREVENTS) { if(window.PointerEvent) { types = [ 'pointerdown', 'pointermove', 'pointerup pointercancel lostpointercapture' ]; } else { types = [ 'MSPointerDown', 'MSPointerMove', 'MSPointerUp MSPointerCancel MSLostPointerCapture' ]; } } else if(Hammer.NO_MOUSEEVENTS) { types = [ 'touchstart', 'touchmove', 'touchend touchcancel' ]; } else { types = [ 'touchstart mousedown', 'touchmove mousemove', 'touchend touchcancel mouseup' ]; } EVENT_TYPES[EVENT_START] = types[0]; EVENT_TYPES[EVENT_MOVE] = types[1]; EVENT_TYPES[EVENT_END] = types[2]; return EVENT_TYPES; }, /** * create touchList depending on the event * @method getTouchList * @param {Object} ev * @param {String} eventType * @return {Array} touches */ getTouchList: function getTouchList(ev, eventType) { // get the fake pointerEvent touchlist if(Hammer.HAS_POINTEREVENTS) { return PointerEvent.getTouchList(); } // get the touchlist if(ev.touches) { if(eventType == EVENT_MOVE) { return ev.touches; } var identifiers = []; var concat = [].concat(Utils.toArray(ev.touches), Utils.toArray(ev.changedTouches)); var touchList = []; Utils.each(concat, function(touch) { if(Utils.inArray(identifiers, touch.identifier) === false) { touchList.push(touch); } identifiers.push(touch.identifier); }); return touchList; } // make fake touchList from mouse position ev.identifier = 1; return [ev]; }, /** * collect basic event data * @method collectEventData * @param {HTMLElement} element * @param {String} eventType matches `EVENT_START|MOVE|END` * @param {Array} touches * @param {Object} ev * @return {Object} ev */ collectEventData: function collectEventData(element, eventType, touches, ev) { // find out pointerType var pointerType = POINTER_TOUCH; if(Utils.inStr(ev.type, 'mouse') || PointerEvent.matchType(POINTER_MOUSE, ev)) { pointerType = POINTER_MOUSE; } else if(PointerEvent.matchType(POINTER_PEN, ev)) { pointerType = POINTER_PEN; } return { center: Utils.getCenter(touches), timeStamp: Date.now(), target: ev.target, touches: touches, eventType: eventType, pointerType: pointerType, srcEvent: ev, /** * prevent the browser default actions * mostly used to disable scrolling of the browser */ preventDefault: function() { var srcEvent = this.srcEvent; srcEvent.preventManipulation && srcEvent.preventManipulation(); srcEvent.preventDefault && srcEvent.preventDefault(); }, /** * stop bubbling the event up to its parents */ stopPropagation: function() { this.srcEvent.stopPropagation(); }, /** * immediately stop gesture detection * might be useful after a swipe was detected * @return {*} */ stopDetect: function() { return Detection.stopDetect(); } }; } }; /** * @module hammer * * @class PointerEvent * @static */ var PointerEvent = Hammer.PointerEvent = { /** * holds all pointers, by `identifier` * @property pointers * @type {Object} */ pointers: {}, /** * get the pointers as an array * @method getTouchList * @return {Array} touchlist */ getTouchList: function getTouchList() { var touchlist = []; // we can use forEach since pointerEvents only is in IE10 Utils.each(this.pointers, function(pointer) { touchlist.push(pointer); }); return touchlist; }, /** * update the position of a pointer * @method updatePointer * @param {String} eventType matches `EVENT_START|MOVE|END` * @param {Object} pointerEvent */ updatePointer: function updatePointer(eventType, pointerEvent) { if(eventType == EVENT_END || (eventType != EVENT_END && pointerEvent.buttons !== 1)) { delete this.pointers[pointerEvent.pointerId]; } else { pointerEvent.identifier = pointerEvent.pointerId; this.pointers[pointerEvent.pointerId] = pointerEvent; } }, /** * check if ev matches pointertype * @method matchType * @param {String} pointerType matches `POINTER_MOUSE|TOUCH|PEN` * @param {PointerEvent} ev */ matchType: function matchType(pointerType, ev) { if(!ev.pointerType) { return false; } var pt = ev.pointerType, types = {}; types[POINTER_MOUSE] = (pt === (ev.MSPOINTER_TYPE_MOUSE || POINTER_MOUSE)); types[POINTER_TOUCH] = (pt === (ev.MSPOINTER_TYPE_TOUCH || POINTER_TOUCH)); types[POINTER_PEN] = (pt === (ev.MSPOINTER_TYPE_PEN || POINTER_PEN)); return types[pointerType]; }, /** * reset the stored pointers * @method reset */ reset: function resetList() { this.pointers = {}; } }; /** * @module hammer * * @class Detection * @static */ var Detection = Hammer.detection = { // contains all registred Hammer.gestures in the correct order gestures: [], // data of the current Hammer.gesture detection session current: null, // the previous Hammer.gesture session data // is a full clone of the previous gesture.current object previous: null, // when this becomes true, no gestures are fired stopped: false, /** * start Hammer.gesture detection * @method startDetect * @param {Hammer.Instance} inst * @param {Object} eventData */ startDetect: function startDetect(inst, eventData) { // already busy with a Hammer.gesture detection on an element if(this.current) { return; } this.stopped = false; // holds current session this.current = { inst: inst, // reference to HammerInstance we're working for startEvent: Utils.extend({}, eventData), // start eventData for distances, timing etc lastEvent: false, // last eventData lastCalcEvent: false, // last eventData for calculations. futureCalcEvent: false, // last eventData for calculations. lastCalcData: {}, // last lastCalcData name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc }; this.detect(eventData); }, /** * Hammer.gesture detection * @method detect * @param {Object} eventData * @return {any} */ detect: function detect(eventData) { if(!this.current || this.stopped) { return; } // extend event data with calculations about scale, distance etc eventData = this.extendEventData(eventData); // hammer instance and instance options var inst = this.current.inst, instOptions = inst.options; // call Hammer.gesture handlers Utils.each(this.gestures, function triggerGesture(gesture) { // only when the instance options have enabled this gesture if(!this.stopped && inst.enabled && instOptions[gesture.name]) { gesture.handler.call(gesture, eventData, inst); } }, this); // store as previous event event if(this.current) { this.current.lastEvent = eventData; } if(eventData.eventType == EVENT_END) { this.stopDetect(); } return eventData; }, /** * clear the Hammer.gesture vars * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected * to stop other Hammer.gestures from being fired * @method stopDetect */ stopDetect: function stopDetect() { // clone current data to the store as the previous gesture // used for the double tap gesture, since this is an other gesture detect session this.previous = Utils.extend({}, this.current); // reset the current this.current = null; this.stopped = true; }, /** * calculate velocity, angle and direction * @method getVelocityData * @param {Object} ev * @param {Object} center * @param {Number} deltaTime * @param {Number} deltaX * @param {Number} deltaY */ getCalculatedData: function getCalculatedData(ev, center, deltaTime, deltaX, deltaY) { var cur = this.current, recalc = false, calcEv = cur.lastCalcEvent, calcData = cur.lastCalcData; if(calcEv && ev.timeStamp - calcEv.timeStamp > Hammer.CALCULATE_INTERVAL) { center = calcEv.center; deltaTime = ev.timeStamp - calcEv.timeStamp; deltaX = ev.center.clientX - calcEv.center.clientX; deltaY = ev.center.clientY - calcEv.center.clientY; recalc = true; } if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) { cur.futureCalcEvent = ev; } if(!cur.lastCalcEvent || recalc) { calcData.velocity = Utils.getVelocity(deltaTime, deltaX, deltaY); calcData.angle = Utils.getAngle(center, ev.center); calcData.direction = Utils.getDirection(center, ev.center); cur.lastCalcEvent = cur.futureCalcEvent || ev; cur.futureCalcEvent = ev; } ev.velocityX = calcData.velocity.x; ev.velocityY = calcData.velocity.y; ev.interimAngle = calcData.angle; ev.interimDirection = calcData.direction; }, /** * extend eventData for Hammer.gestures * @method extendEventData * @param {Object} ev * @return {Object} ev */ extendEventData: function extendEventData(ev) { var cur = this.current, startEv = cur.startEvent, lastEv = cur.lastEvent || startEv; // update the start touchlist to calculate the scale/rotation if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) { startEv.touches = []; Utils.each(ev.touches, function(touch) { startEv.touches.push({ clientX: touch.clientX, clientY: touch.clientY }); }); } var deltaTime = ev.timeStamp - startEv.timeStamp, deltaX = ev.center.clientX - startEv.center.clientX, deltaY = ev.center.clientY - startEv.center.clientY; this.getCalculatedData(ev, lastEv.center, deltaTime, deltaX, deltaY); Utils.extend(ev, { startEvent: startEv, deltaTime: deltaTime, deltaX: deltaX, deltaY: deltaY, distance: Utils.getDistance(startEv.center, ev.center), angle: Utils.getAngle(startEv.center, ev.center), direction: Utils.getDirection(startEv.center, ev.center), scale: Utils.getScale(startEv.touches, ev.touches), rotation: Utils.getRotation(startEv.touches, ev.touches) }); return ev; }, /** * register new gesture * @method register * @param {Object} gesture object, see `gestures/` for documentation * @return {Array} gestures */ register: function register(gesture) { // add an enable gesture options if there is no given var options = gesture.defaults || {}; if(options[gesture.name] === undefined) { options[gesture.name] = true; } // extend Hammer default options with the Hammer.gesture options Utils.extend(Hammer.defaults, options, true); // set its index gesture.index = gesture.index || 1000; // add Hammer.gesture to the list this.gestures.push(gesture); // sort the list by index this.gestures.sort(function(a, b) { if(a.index < b.index) { return -1; } if(a.index > b.index) { return 1; } return 0; }); return this.gestures; } }; /** * @module hammer */ /** * create new hammer instance * all methods should return the instance itself, so it is chainable. * * @class Instance * @constructor * @param {HTMLElement} element * @param {Object} [options={}] options are merged with `Hammer.defaults` * @return {Hammer.Instance} */ Hammer.Instance = function(element, options) { var self = this; // setup HammerJS window events and register all gestures // this also sets up the default options setup(); /** * @property element * @type {HTMLElement} */ this.element = element; /** * @property enabled * @type {Boolean} * @protected */ this.enabled = true; /** * options, merged with the defaults * options with an _ are converted to camelCase * @property options * @type {Object} */ Utils.each(options, function(value, name) { delete options[name]; options[Utils.toCamelCase(name)] = value; }); this.options = Utils.extend(Utils.extend({}, Hammer.defaults), options || {}); // add some css to the element to prevent the browser from doing its native behavoir if(this.options.behavior) { Utils.toggleBehavior(this.element, this.options.behavior, true); } /** * event start handler on the element to start the detection * @property eventStartHandler * @type {Object} */ this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) { if(self.enabled && ev.eventType == EVENT_START) { Detection.startDetect(self, ev); } else if(ev.eventType == EVENT_TOUCH) { Detection.detect(ev); } }); /** * keep a list of user event handlers which needs to be removed when calling 'dispose' * @property eventHandlers * @type {Array} */ this.eventHandlers = []; }; Hammer.Instance.prototype = { /** * bind events to the instance * @method on * @chainable * @param {String} gestures multiple gestures by splitting with a space * @param {Function} handler * @param {Object} handler.ev event object */ on: function onEvent(gestures, handler) { var self = this; Event.on(self.element, gestures, handler, function(type) { self.eventHandlers.push({ gesture: type, handler: handler }); }); return self; }, /** * unbind events to the instance * @method off * @chainable * @param {String} gestures * @param {Function} handler */ off: function offEvent(gestures, handler) { var self = this; Event.off(self.element, gestures, handler, function(type) { var index = Utils.inArray({ gesture: type, handler: handler }); if(index !== false) { self.eventHandlers.splice(index, 1); } }); return self; }, /** * trigger gesture event * @method trigger * @chainable * @param {String} gesture * @param {Object} [eventData] */ trigger: function triggerEvent(gesture, eventData) { // optional if(!eventData) { eventData = {}; } // create DOM event var event = Hammer.DOCUMENT.createEvent('Event'); event.initEvent(gesture, true, true); event.gesture = eventData; // trigger on the target if it is in the instance element, // this is for event delegation tricks var element = this.element; if(Utils.hasParent(eventData.target, element)) { element = eventData.target; } element.dispatchEvent(event); return this; }, /** * enable of disable hammer.js detection * @method enable * @chainable * @param {Boolean} state */ enable: function enable(state) { this.enabled = state; return this; }, /** * dispose this hammer instance * @method dispose * @return {Null} */ dispose: function dispose() { var i, eh; // undo all changes made by stop_browser_behavior Utils.toggleBehavior(this.element, this.options.behavior, false); // unbind all custom event handlers for(i = -1; (eh = this.eventHandlers[++i]);) { Utils.off(this.element, eh.gesture, eh.handler); } this.eventHandlers = []; // unbind the start event listener Event.off(this.element, EVENT_TYPES[EVENT_START], this.eventStartHandler); return null; } }; /** * @module gestures */ /** * Move with x fingers (default 1) around on the page. * Preventing the default browser behavior is a good way to improve feel and working. * ```` * hammertime.on("drag", function(ev) { * console.log(ev); * ev.gesture.preventDefault(); * }); * ```` * * @class Drag * @static */ /** * @event drag * @param {Object} ev */ /** * @event dragstart * @param {Object} ev */ /** * @event dragend * @param {Object} ev */ /** * @event drapleft * @param {Object} ev */ /** * @event dragright * @param {Object} ev */ /** * @event dragup * @param {Object} ev */ /** * @event dragdown * @param {Object} ev */ /** * @param {String} name */ (function(name) { var triggered = false; function dragGesture(ev, inst) { var cur = Detection.current; // max touches if(inst.options.dragMaxTouches > 0 && ev.touches.length > inst.options.dragMaxTouches) { return; } switch(ev.eventType) { case EVENT_START: triggered = false; break; case EVENT_MOVE: // when the distance we moved is too small we skip this gesture // or we can be already in dragging if(ev.distance < inst.options.dragMinDistance && cur.name != name) { return; } var startCenter = cur.startEvent.center; // we are dragging! if(cur.name != name) { cur.name = name; if(inst.options.dragDistanceCorrection && ev.distance > 0) { // When a drag is triggered, set the event center to dragMinDistance pixels from the original event center. // Without this correction, the dragged distance would jumpstart at dragMinDistance pixels instead of at 0. // It might be useful to save the original start point somewhere var factor = Math.abs(inst.options.dragMinDistance / ev.distance); startCenter.pageX += ev.deltaX * factor; startCenter.pageY += ev.deltaY * factor; startCenter.clientX += ev.deltaX * factor; startCenter.clientY += ev.deltaY * factor; // recalculate event data using new start point ev = Detection.extendEventData(ev); } } // lock drag to axis? if(cur.lastEvent.dragLockToAxis || ( inst.options.dragLockToAxis && inst.options.dragLockMinDistance <= ev.distance )) { ev.dragLockToAxis = true; } // keep direction on the axis that the drag gesture started on var lastDirection = cur.lastEvent.direction; if(ev.dragLockToAxis && lastDirection !== ev.direction) { if(Utils.isVertical(lastDirection)) { ev.direction = (ev.deltaY < 0) ? DIRECTION_UP : DIRECTION_DOWN; } else { ev.direction = (ev.deltaX < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; } } // first time, trigger dragstart event if(!triggered) { inst.trigger(name + 'start', ev); triggered = true; } // trigger events inst.trigger(name, ev); inst.trigger(name + ev.direction, ev); var isVertical = Utils.isVertical(ev.direction); // block the browser events if((inst.options.dragBlockVertical && isVertical) || (inst.options.dragBlockHorizontal && !isVertical)) { ev.preventDefault(); } break; case EVENT_RELEASE: if(triggered && ev.changedLength <= inst.options.dragMaxTouches) { inst.trigger(name + 'end', ev); triggered = false; } break; case EVENT_END: triggered = false; break; } } Hammer.gestures.Drag = { name: name, index: 50, handler: dragGesture, defaults: { /** * minimal movement that have to be made before the drag event gets triggered * @property dragMinDistance * @type {Number} * @default 10 */ dragMinDistance: 10, /** * Set dragDistanceCorrection to true to make the starting point of the drag * be calculated from where the drag was triggered, not from where the touch started. * Useful to avoid a jerk-starting drag, which can make fine-adjustments * through dragging difficult, and be visually unappealing. * @property dragDistanceCorrection * @type {Boolean} * @default true */ dragDistanceCorrection: true, /** * set 0 for unlimited, but this can conflict with transform * @property dragMaxTouches * @type {Number} * @default 1 */ dragMaxTouches: 1, /** * prevent default browser behavior when dragging occurs * be careful with it, it makes the element a blocking element * when you are using the drag gesture, it is a good practice to set this true * @property dragBlockHorizontal * @type {Boolean} * @default false */ dragBlockHorizontal: false, /** * same as `dragBlockHorizontal`, but for vertical movement * @property dragBlockVertical * @type {Boolean} * @default false */ dragBlockVertical: false, /** * dragLockToAxis keeps the drag gesture on the axis that it started on, * It disallows vertical directions if the initial direction was horizontal, and vice versa. * @property dragLockToAxis * @type {Boolean} * @default false */ dragLockToAxis: false, /** * drag lock only kicks in when distance > dragLockMinDistance * This way, locking occurs only when the distance has become large enough to reliably determine the direction * @property dragLockMinDistance * @type {Number} * @default 25 */ dragLockMinDistance: 25 } }; })('drag'); /** * @module gestures */ /** * trigger a simple gesture event, so you can do anything in your handler. * only usable if you know what your doing... * * @class Gesture * @static */ /** * @event gesture * @param {Object} ev */ Hammer.gestures.Gesture = { name: 'gesture', index: 1337, handler: function releaseGesture(ev, inst) { inst.trigger(this.name, ev); } }; /** * @module gestures */ /** * Touch stays at the same place for x time * * @class Hold * @static */ /** * @event hold * @param {Object} ev */ /** * @param {String} name */ (function(name) { var timer; function holdGesture(ev, inst) { var options = inst.options, current = Detection.current; switch(ev.eventType) { case EVENT_START: clearTimeout(timer); // set the gesture so we can check in the timeout if it still is current.name = name; // set timer and if after the timeout it still is hold, // we trigger the hold event timer = setTimeout(function() { if(current && current.name == name) { inst.trigger(name, ev); } }, options.holdTimeout); break; case EVENT_MOVE: if(ev.distance > options.holdThreshold) { clearTimeout(timer); } break; case EVENT_RELEASE: clearTimeout(timer); break; } } Hammer.gestures.Hold = { name: name, index: 10, defaults: { /** * @property holdTimeout * @type {Number} * @default 500 */ holdTimeout: 500, /** * movement allowed while holding * @property holdThreshold * @type {Number} * @default 2 */ holdThreshold: 2 }, handler: holdGesture }; })('hold'); /** * @module gestures */ /** * when a touch is being released from the page * * @class Release * @static */ /** * @event release * @param {Object} ev */ Hammer.gestures.Release = { name: 'release', index: Infinity, handler: function releaseGesture(ev, inst) { if(ev.eventType == EVENT_RELEASE) { inst.trigger(this.name, ev); } } }; /** * @module gestures */ /** * triggers swipe events when the end velocity is above the threshold * for best usage, set `preventDefault` (on the drag gesture) to `true` * ```` * hammertime.on("dragleft swipeleft", function(ev) { * console.log(ev); * ev.gesture.preventDefault(); * }); * ```` * * @class Swipe * @static */ /** * @event swipe * @param {Object} ev */ /** * @event swipeleft * @param {Object} ev */ /** * @event swiperight * @param {Object} ev */ /** * @event swipeup * @param {Object} ev */ /** * @event swipedown * @param {Object} ev */ Hammer.gestures.Swipe = { name: 'swipe', index: 40, defaults: { /** * @property swipeMinTouches * @type {Number} * @default 1 */ swipeMinTouches: 1, /** * @property swipeMaxTouches * @type {Number} * @default 1 */ swipeMaxTouches: 1, /** * horizontal swipe velocity * @property swipeVelocityX * @type {Number} * @default 0.6 */ swipeVelocityX: 0.6, /** * vertical swipe velocity * @property swipeVelocityY * @type {Number} * @default 0.6 */ swipeVelocityY: 0.6 }, handler: function swipeGesture(ev, inst) { if(ev.eventType == EVENT_RELEASE) { var touches = ev.touches.length, options = inst.options; // max touches if(touches < options.swipeMinTouches || touches > options.swipeMaxTouches) { return; } // when the distance we moved is too small we skip this gesture // or we can be already in dragging if(ev.velocityX > options.swipeVelocityX || ev.velocityY > options.swipeVelocityY) { // trigger swipe events inst.trigger(this.name, ev); inst.trigger(this.name + ev.direction, ev); } } } }; /** * @module gestures */ /** * Single tap and a double tap on a place * * @class Tap * @static */ /** * @event tap * @param {Object} ev */ /** * @event doubletap * @param {Object} ev */ /** * @param {String} name */ (function(name) { var hasMoved = false; function tapGesture(ev, inst) { var options = inst.options, current = Detection.current, prev = Detection.previous, sincePrev, didDoubleTap; switch(ev.eventType) { case EVENT_START: hasMoved = false; break; case EVENT_MOVE: hasMoved = hasMoved || (ev.distance > options.tapMaxDistance); break; case EVENT_END: if(!Utils.inStr(ev.srcEvent.type, 'cancel') && ev.deltaTime < options.tapMaxTime && !hasMoved) { // previous gesture, for the double tap since these are two different gesture detections sincePrev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp; didDoubleTap = false; // check if double tap if(prev && prev.name == name && (sincePrev && sincePrev < options.doubleTapInterval) && ev.distance < options.doubleTapDistance) { inst.trigger('doubletap', ev); didDoubleTap = true; } // do a single tap if(!didDoubleTap || options.tapAlways) { current.name = name; inst.trigger(current.name, ev); } } break; } } Hammer.gestures.Tap = { name: name, index: 100, handler: tapGesture, defaults: { /** * max time of a tap, this is for the slow tappers * @property tapMaxTime * @type {Number} * @default 250 */ tapMaxTime: 250, /** * max distance of movement of a tap, this is for the slow tappers * @property tapMaxDistance * @type {Number} * @default 10 */ tapMaxDistance: 10, /** * always trigger the `tap` event, even while double-tapping * @property tapAlways * @type {Boolean} * @default true */ tapAlways: true, /** * max distance between two taps * @property doubleTapDistance * @type {Number} * @default 20 */ doubleTapDistance: 20, /** * max time between two taps * @property doubleTapInterval * @type {Number} * @default 300 */ doubleTapInterval: 300 } }; })('tap'); /** * @module gestures */ /** * when a touch is being touched at the page * * @class Touch * @static */ /** * @event touch * @param {Object} ev */ Hammer.gestures.Touch = { name: 'touch', index: -Infinity, defaults: { /** * call preventDefault at touchstart, and makes the element blocking by disabling the scrolling of the page, * but it improves gestures like transforming and dragging. * be careful with using this, it can be very annoying for users to be stuck on the page * @property preventDefault * @type {Boolean} * @default false */ preventDefault: false, /** * disable mouse events, so only touch (or pen!) input triggers events * @property preventMouse * @type {Boolean} * @default false */ preventMouse: false }, handler: function touchGesture(ev, inst) { if(inst.options.preventMouse && ev.pointerType == POINTER_MOUSE) { ev.stopDetect(); return; } if(inst.options.preventDefault) { ev.preventDefault(); } if(ev.eventType == EVENT_TOUCH) { inst.trigger('touch', ev); } } }; /** * @module gestures */ /** * User want to scale or rotate with 2 fingers * Preventing the default browser behavior is a good way to improve feel and working. This can be done with the * `preventDefault` option. * * @class Transform * @static */ /** * @event transform * @param {Object} ev */ /** * @event transformstart * @param {Object} ev */ /** * @event transformend * @param {Object} ev */ /** * @event pinchin * @param {Object} ev */ /** * @event pinchout * @param {Object} ev */ /** * @event rotate * @param {Object} ev */ /** * @param {String} name */ (function(name) { var triggered = false; function transformGesture(ev, inst) { switch(ev.eventType) { case EVENT_START: triggered = false; break; case EVENT_MOVE: // at least multitouch if(ev.touches.length < 2) { return; } var scaleThreshold = Math.abs(1 - ev.scale); var rotationThreshold = Math.abs(ev.rotation); // when the distance we moved is too small we skip this gesture // or we can be already in dragging if(scaleThreshold < inst.options.transformMinScale && rotationThreshold < inst.options.transformMinRotation) { return; } // we are transforming! Detection.current.name = name; // first time, trigger dragstart event if(!triggered) { inst.trigger(name + 'start', ev); triggered = true; } inst.trigger(name, ev); // basic transform event // trigger rotate event if(rotationThreshold > inst.options.transformMinRotation) { inst.trigger('rotate', ev); } // trigger pinch event if(scaleThreshold > inst.options.transformMinScale) { inst.trigger('pinch', ev); inst.trigger('pinch' + (ev.scale < 1 ? 'in' : 'out'), ev); } break; case EVENT_RELEASE: if(triggered && ev.changedLength < 2) { inst.trigger(name + 'end', ev); triggered = false; } break; } } Hammer.gestures.Transform = { name: name, index: 45, defaults: { /** * minimal scale factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 * @property transformMinScale * @type {Number} * @default 0.01 */ transformMinScale: 0.01, /** * rotation in degrees * @property transformMinRotation * @type {Number} * @default 1 */ transformMinRotation: 1 }, handler: transformGesture }; })('transform'); /** * @module hammer */ // AMD export if(typeof define == 'function' && define.amd) { define(function() { return Hammer; }); // commonjs export } else if(typeof module !== 'undefined' && module.exports) { module.exports = Hammer; // browser export } else { window.Hammer = Hammer; } })(window); },{}],32:[function(_dereq_,module,exports){ if (!('indexOf' in Array.prototype)) { Array.prototype.indexOf= function(find, i /*opt*/) { if (i===undefined) i= 0; if (i<0) i+= this.length; if (i<0) i= 0; for (var n= this.length; i= 0) { name = type.substring(i + 1); type = type.substring(0, i); } if (type) return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener); if (arguments.length === 2) { if (listener == null) for (type in this) { if (this.hasOwnProperty(type)) this[type].on(name, null); } return this; } }; function d3_dispatch_event(dispatch) { var listeners = [], listenerByName = new d3_Map; function event() { var z = listeners, // defensive reference i = -1, n = z.length, l; while (++i < n) if (l = z[i].on) l.apply(this, arguments); return dispatch; } event.on = function(name, listener) { var l = listenerByName.get(name), i; // return the current listener, if any if (arguments.length < 2) return l && l.on; // remove the old listener, if any (with copy-on-write) if (l) { l.on = null; listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1)); listenerByName.remove(name); } // add the new listener, if any if (listener) listeners.push(listenerByName.set(name, {on: listener})); return dispatch; }; return event; } // Copies a variable number of methods from source to target. d3.rebind = function(target, source) { var i = 1, n = arguments.length, method; while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]); return target; }; // Method is assumed to be a standard D3 getter-setter: // If passed with no arguments, gets the value. // If passed with arguments, sets the value and returns the target. function d3_rebind(target, source, method) { return function() { var value = method.apply(source, arguments); return value === source ? target : value; }; } return d3; })(); },{}],33:[function(_dereq_,module,exports){ // Released under MIT license // Copyright (c) 2009-2010 Dominic Baggott // Copyright (c) 2009-2010 Ash Berlin // Copyright (c) 2011 Christoph Dorn (http://www.christophdorn.com) // Date: 2013-09-15T16:12Z (function(expose) { var MarkdownHelpers = {}; // For Spidermonkey based engines function mk_block_toSource() { return "Markdown.mk_block( " + uneval(this.toString()) + ", " + uneval(this.trailing) + ", " + uneval(this.lineNumber) + " )"; } // node function mk_block_inspect() { var util = _dereq_("util"); return "Markdown.mk_block( " + util.inspect(this.toString()) + ", " + util.inspect(this.trailing) + ", " + util.inspect(this.lineNumber) + " )"; } MarkdownHelpers.mk_block = function(block, trail, line) { // Be helpful for default case in tests. if ( arguments.length === 1 ) trail = "\n\n"; // We actually need a String object, not a string primitive /* jshint -W053 */ var s = new String(block); s.trailing = trail; // To make it clear its not just a string s.inspect = mk_block_inspect; s.toSource = mk_block_toSource; if ( line !== undefined ) s.lineNumber = line; return s; }; var isArray = MarkdownHelpers.isArray = Array.isArray || function(obj) { return Object.prototype.toString.call(obj) === "[object Array]"; }; // Don't mess with Array.prototype. Its not friendly if ( Array.prototype.forEach ) { MarkdownHelpers.forEach = function forEach( arr, cb, thisp ) { return arr.forEach( cb, thisp ); }; } else { MarkdownHelpers.forEach = function forEach(arr, cb, thisp) { for (var i = 0; i < arr.length; i++) cb.call(thisp || arr, arr[i], i, arr); }; } MarkdownHelpers.isEmpty = function isEmpty( obj ) { for ( var key in obj ) { if ( hasOwnProperty.call( obj, key ) ) return false; } return true; }; MarkdownHelpers.extract_attr = function extract_attr( jsonml ) { return isArray(jsonml) && jsonml.length > 1 && typeof jsonml[ 1 ] === "object" && !( isArray(jsonml[ 1 ]) ) ? jsonml[ 1 ] : undefined; }; /** * class Markdown * * Markdown processing in Javascript done right. We have very particular views * on what constitutes 'right' which include: * * - produces well-formed HTML (this means that em and strong nesting is * important) * * - has an intermediate representation to allow processing of parsed data (We * in fact have two, both as [JsonML]: a markdown tree and an HTML tree). * * - is easily extensible to add new dialects without having to rewrite the * entire parsing mechanics * * - has a good test suite * * This implementation fulfills all of these (except that the test suite could * do with expanding to automatically run all the fixtures from other Markdown * implementations.) * * ##### Intermediate Representation * * *TODO* Talk about this :) Its JsonML, but document the node names we use. * * [JsonML]: http://jsonml.org/ "JSON Markup Language" **/ var Markdown = function(dialect) { switch (typeof dialect) { case "undefined": this.dialect = Markdown.dialects.Gruber; break; case "object": this.dialect = dialect; break; default: if ( dialect in Markdown.dialects ) this.dialect = Markdown.dialects[dialect]; else throw new Error("Unknown Markdown dialect '" + String(dialect) + "'"); break; } this.em_state = []; this.strong_state = []; this.debug_indent = ""; }; /** * Markdown.dialects * * Namespace of built-in dialects. **/ Markdown.dialects = {}; // Imported functions var mk_block = Markdown.mk_block = MarkdownHelpers.mk_block, isArray = MarkdownHelpers.isArray; /** * parse( markdown, [dialect] ) -> JsonML * - markdown (String): markdown string to parse * - dialect (String | Dialect): the dialect to use, defaults to gruber * * Parse `markdown` and return a markdown document as a Markdown.JsonML tree. **/ Markdown.parse = function( source, dialect ) { // dialect will default if undefined var md = new Markdown( dialect ); return md.toTree( source ); }; function count_lines( str ) { var n = 0, i = -1; while ( ( i = str.indexOf("\n", i + 1) ) !== -1 ) n++; return n; } // Internal - split source into rough blocks Markdown.prototype.split_blocks = function splitBlocks( input ) { input = input.replace(/(\r\n|\n|\r)/g, "\n"); // [\s\S] matches _anything_ (newline or space) // [^] is equivalent but doesn't work in IEs. var re = /([\s\S]+?)($|\n#|\n(?:\s*\n|$)+)/g, blocks = [], m; var line_no = 1; if ( ( m = /^(\s*\n)/.exec(input) ) !== null ) { // skip (but count) leading blank lines line_no += count_lines( m[0] ); re.lastIndex = m[0].length; } while ( ( m = re.exec(input) ) !== null ) { if (m[2] === "\n#") { m[2] = "\n"; re.lastIndex--; } blocks.push( mk_block( m[1], m[2], line_no ) ); line_no += count_lines( m[0] ); } return blocks; }; /** * Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ] * - block (String): the block to process * - next (Array): the following blocks * * Process `block` and return an array of JsonML nodes representing `block`. * * It does this by asking each block level function in the dialect to process * the block until one can. Succesful handling is indicated by returning an * array (with zero or more JsonML nodes), failure by a false value. * * Blocks handlers are responsible for calling [[Markdown#processInline]] * themselves as appropriate. * * If the blocks were split incorrectly or adjacent blocks need collapsing you * can adjust `next` in place using shift/splice etc. * * If any of this default behaviour is not right for the dialect, you can * define a `__call__` method on the dialect that will get invoked to handle * the block processing. */ Markdown.prototype.processBlock = function processBlock( block, next ) { var cbs = this.dialect.block, ord = cbs.__order__; if ( "__call__" in cbs ) return cbs.__call__.call(this, block, next); for ( var i = 0; i < ord.length; i++ ) { //D:this.debug( "Testing", ord[i] ); var res = cbs[ ord[i] ].call( this, block, next ); if ( res ) { //D:this.debug(" matched"); if ( !isArray(res) || ( res.length > 0 && !( isArray(res[0]) ) ) ) this.debug(ord[i], "didn't return a proper array"); //D:this.debug( "" ); return res; } } // Uhoh! no match! Should we throw an error? return []; }; Markdown.prototype.processInline = function processInline( block ) { return this.dialect.inline.__call__.call( this, String( block ) ); }; /** * Markdown#toTree( source ) -> JsonML * - source (String): markdown source to parse * * Parse `source` into a JsonML tree representing the markdown document. **/ // custom_tree means set this.tree to `custom_tree` and restore old value on return Markdown.prototype.toTree = function toTree( source, custom_root ) { var blocks = source instanceof Array ? source : this.split_blocks( source ); // Make tree a member variable so its easier to mess with in extensions var old_tree = this.tree; try { this.tree = custom_root || this.tree || [ "markdown" ]; blocks_loop: while ( blocks.length ) { var b = this.processBlock( blocks.shift(), blocks ); // Reference blocks and the like won't return any content if ( !b.length ) continue blocks_loop; this.tree.push.apply( this.tree, b ); } return this.tree; } finally { if ( custom_root ) this.tree = old_tree; } }; // Noop by default Markdown.prototype.debug = function () { var args = Array.prototype.slice.call( arguments); args.unshift(this.debug_indent); if ( typeof print !== "undefined" ) print.apply( print, args ); if ( typeof console !== "undefined" && typeof console.log !== "undefined" ) console.log.apply( null, args ); }; Markdown.prototype.loop_re_over_block = function( re, block, cb ) { // Dont use /g regexps with this var m, b = block.valueOf(); while ( b.length && (m = re.exec(b) ) !== null ) { b = b.substr( m[0].length ); cb.call(this, m); } return b; }; // Build default order from insertion order. Markdown.buildBlockOrder = function(d) { var ord = []; for ( var i in d ) { if ( i === "__order__" || i === "__call__" ) continue; ord.push( i ); } d.__order__ = ord; }; // Build patterns for inline matcher Markdown.buildInlinePatterns = function(d) { var patterns = []; for ( var i in d ) { // __foo__ is reserved and not a pattern if ( i.match( /^__.*__$/) ) continue; var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" ) .replace( /\n/, "\\n" ); patterns.push( i.length === 1 ? l : "(?:" + l + ")" ); } patterns = patterns.join("|"); d.__patterns__ = patterns; //print("patterns:", uneval( patterns ) ); var fn = d.__call__; d.__call__ = function(text, pattern) { if ( pattern !== undefined ) return fn.call(this, text, pattern); else return fn.call(this, text, patterns); }; }; var extract_attr = MarkdownHelpers.extract_attr; /** * renderJsonML( jsonml[, options] ) -> String * - jsonml (Array): JsonML array to render to XML * - options (Object): options * * Converts the given JsonML into well-formed XML. * * The options currently understood are: * * - root (Boolean): wether or not the root node should be included in the * output, or just its children. The default `false` is to not include the * root itself. */ Markdown.renderJsonML = function( jsonml, options ) { options = options || {}; // include the root element in the rendered output? options.root = options.root || false; var content = []; if ( options.root ) { content.push( render_tree( jsonml ) ); } else { jsonml.shift(); // get rid of the tag if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) jsonml.shift(); // get rid of the attributes while ( jsonml.length ) content.push( render_tree( jsonml.shift() ) ); } return content.join( "\n\n" ); }; /** * toHTMLTree( markdown, [dialect] ) -> JsonML * toHTMLTree( md_tree ) -> JsonML * - markdown (String): markdown string to parse * - dialect (String | Dialect): the dialect to use, defaults to gruber * - md_tree (Markdown.JsonML): parsed markdown tree * * Turn markdown into HTML, represented as a JsonML tree. If a string is given * to this function, it is first parsed into a markdown tree by calling * [[parse]]. **/ Markdown.toHTMLTree = function toHTMLTree( input, dialect , options ) { // convert string input to an MD tree if ( typeof input === "string" ) input = this.parse( input, dialect ); // Now convert the MD tree to an HTML tree // remove references from the tree var attrs = extract_attr( input ), refs = {}; if ( attrs && attrs.references ) refs = attrs.references; var html = convert_tree_to_html( input, refs , options ); merge_text_nodes( html ); return html; }; /** * toHTML( markdown, [dialect] ) -> String * toHTML( md_tree ) -> String * - markdown (String): markdown string to parse * - md_tree (Markdown.JsonML): parsed markdown tree * * Take markdown (either as a string or as a JsonML tree) and run it through * [[toHTMLTree]] then turn it into a well-formated HTML fragment. **/ Markdown.toHTML = function toHTML( source , dialect , options ) { var input = this.toHTMLTree( source , dialect , options ); return this.renderJsonML( input ); }; function escapeHTML( text ) { return text.replace( /&/g, "&" ) .replace( //g, ">" ) .replace( /"/g, """ ) .replace( /'/g, "'" ); } function render_tree( jsonml ) { // basic case if ( typeof jsonml === "string" ) return escapeHTML( jsonml ); var tag = jsonml.shift(), attributes = {}, content = []; if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) attributes = jsonml.shift(); while ( jsonml.length ) content.push( render_tree( jsonml.shift() ) ); var tag_attrs = ""; for ( var a in attributes ) tag_attrs += " " + a + '="' + escapeHTML( attributes[ a ] ) + '"'; // be careful about adding whitespace here for inline elements if ( tag === "img" || tag === "br" || tag === "hr" ) return "<"+ tag + tag_attrs + "/>"; else return "<"+ tag + tag_attrs + ">" + content.join( "" ) + ""; } function convert_tree_to_html( tree, references, options ) { var i; options = options || {}; // shallow clone var jsonml = tree.slice( 0 ); if ( typeof options.preprocessTreeNode === "function" ) jsonml = options.preprocessTreeNode(jsonml, references); // Clone attributes if they exist var attrs = extract_attr( jsonml ); if ( attrs ) { jsonml[ 1 ] = {}; for ( i in attrs ) { jsonml[ 1 ][ i ] = attrs[ i ]; } attrs = jsonml[ 1 ]; } // basic case if ( typeof jsonml === "string" ) return jsonml; // convert this node switch ( jsonml[ 0 ] ) { case "header": jsonml[ 0 ] = "h" + jsonml[ 1 ].level; delete jsonml[ 1 ].level; break; case "bulletlist": jsonml[ 0 ] = "ul"; break; case "numberlist": jsonml[ 0 ] = "ol"; break; case "listitem": jsonml[ 0 ] = "li"; break; case "para": jsonml[ 0 ] = "p"; break; case "markdown": jsonml[ 0 ] = "html"; if ( attrs ) delete attrs.references; break; case "code_block": jsonml[ 0 ] = "pre"; i = attrs ? 2 : 1; var code = [ "code" ]; code.push.apply( code, jsonml.splice( i, jsonml.length - i ) ); jsonml[ i ] = code; break; case "inlinecode": jsonml[ 0 ] = "code"; break; case "img": jsonml[ 1 ].src = jsonml[ 1 ].href; delete jsonml[ 1 ].href; break; case "linebreak": jsonml[ 0 ] = "br"; break; case "link": jsonml[ 0 ] = "a"; break; case "link_ref": jsonml[ 0 ] = "a"; // grab this ref and clean up the attribute node var ref = references[ attrs.ref ]; // if the reference exists, make the link if ( ref ) { delete attrs.ref; // add in the href and title, if present attrs.href = ref.href; if ( ref.title ) attrs.title = ref.title; // get rid of the unneeded original text delete attrs.original; } // the reference doesn't exist, so revert to plain text else { return attrs.original; } break; case "img_ref": jsonml[ 0 ] = "img"; // grab this ref and clean up the attribute node var ref = references[ attrs.ref ]; // if the reference exists, make the link if ( ref ) { delete attrs.ref; // add in the href and title, if present attrs.src = ref.href; if ( ref.title ) attrs.title = ref.title; // get rid of the unneeded original text delete attrs.original; } // the reference doesn't exist, so revert to plain text else { return attrs.original; } break; } // convert all the children i = 1; // deal with the attribute node, if it exists if ( attrs ) { // if there are keys, skip over it for ( var key in jsonml[ 1 ] ) { i = 2; break; } // if there aren't, remove it if ( i === 1 ) jsonml.splice( i, 1 ); } for ( ; i < jsonml.length; ++i ) { jsonml[ i ] = convert_tree_to_html( jsonml[ i ], references, options ); } return jsonml; } // merges adjacent text nodes into a single node function merge_text_nodes( jsonml ) { // skip the tag name and attribute hash var i = extract_attr( jsonml ) ? 2 : 1; while ( i < jsonml.length ) { // if it's a string check the next item too if ( typeof jsonml[ i ] === "string" ) { if ( i + 1 < jsonml.length && typeof jsonml[ i + 1 ] === "string" ) { // merge the second string into the first and remove it jsonml[ i ] += jsonml.splice( i + 1, 1 )[ 0 ]; } else { ++i; } } // if it's not a string recurse else { merge_text_nodes( jsonml[ i ] ); ++i; } } }; var DialectHelpers = {}; DialectHelpers.inline_until_char = function( text, want ) { var consumed = 0, nodes = []; while ( true ) { if ( text.charAt( consumed ) === want ) { // Found the character we were looking for consumed++; return [ consumed, nodes ]; } if ( consumed >= text.length ) { // No closing char found. Abort. return null; } var res = this.dialect.inline.__oneElement__.call(this, text.substr( consumed ) ); consumed += res[ 0 ]; // Add any returned nodes. nodes.push.apply( nodes, res.slice( 1 ) ); } }; // Helper function to make sub-classing a dialect easier DialectHelpers.subclassDialect = function( d ) { function Block() {} Block.prototype = d.block; function Inline() {} Inline.prototype = d.inline; return { block: new Block(), inline: new Inline() }; }; var forEach = MarkdownHelpers.forEach, extract_attr = MarkdownHelpers.extract_attr, mk_block = MarkdownHelpers.mk_block, isEmpty = MarkdownHelpers.isEmpty, inline_until_char = DialectHelpers.inline_until_char; /** * Gruber dialect * * The default dialect that follows the rules set out by John Gruber's * markdown.pl as closely as possible. Well actually we follow the behaviour of * that script which in some places is not exactly what the syntax web page * says. **/ var Gruber = { block: { atxHeader: function atxHeader( block, next ) { var m = block.match( /^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/ ); if ( !m ) return undefined; var header = [ "header", { level: m[ 1 ].length } ]; Array.prototype.push.apply(header, this.processInline(m[ 2 ])); if ( m[0].length < block.length ) next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) ); return [ header ]; }, setextHeader: function setextHeader( block, next ) { var m = block.match( /^(.*)\n([-=])\2\2+(?:\n|$)/ ); if ( !m ) return undefined; var level = ( m[ 2 ] === "=" ) ? 1 : 2, header = [ "header", { level : level }, m[ 1 ] ]; if ( m[0].length < block.length ) next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) ); return [ header ]; }, code: function code( block, next ) { // | Foo // |bar // should be a code block followed by a paragraph. Fun // // There might also be adjacent code block to merge. var ret = [], re = /^(?: {0,3}\t| {4})(.*)\n?/; // 4 spaces + content if ( !block.match( re ) ) return undefined; block_search: do { // Now pull out the rest of the lines var b = this.loop_re_over_block( re, block.valueOf(), function( m ) { ret.push( m[1] ); } ); if ( b.length ) { // Case alluded to in first comment. push it back on as a new block next.unshift( mk_block(b, block.trailing) ); break block_search; } else if ( next.length ) { // Check the next block - it might be code too if ( !next[0].match( re ) ) break block_search; // Pull how how many blanks lines follow - minus two to account for .join ret.push ( block.trailing.replace(/[^\n]/g, "").substring(2) ); block = next.shift(); } else { break block_search; } } while ( true ); return [ [ "code_block", ret.join("\n") ] ]; }, horizRule: function horizRule( block, next ) { // this needs to find any hr in the block to handle abutting blocks var m = block.match( /^(?:([\s\S]*?)\n)?[ \t]*([-_*])(?:[ \t]*\2){2,}[ \t]*(?:\n([\s\S]*))?$/ ); if ( !m ) return undefined; var jsonml = [ [ "hr" ] ]; // if there's a leading abutting block, process it if ( m[ 1 ] ) { var contained = mk_block( m[ 1 ], "", block.lineNumber ); jsonml.unshift.apply( jsonml, this.toTree( contained, [] ) ); } // if there's a trailing abutting block, stick it into next if ( m[ 3 ] ) next.unshift( mk_block( m[ 3 ], block.trailing, block.lineNumber + 1 ) ); return jsonml; }, // There are two types of lists. Tight and loose. Tight lists have no whitespace // between the items (and result in text just in the
  • ) and loose lists, // which have an empty line between list items, resulting in (one or more) // paragraphs inside the
  • . // // There are all sorts weird edge cases about the original markdown.pl's // handling of lists: // // * Nested lists are supposed to be indented by four chars per level. But // if they aren't, you can get a nested list by indenting by less than // four so long as the indent doesn't match an indent of an existing list // item in the 'nest stack'. // // * The type of the list (bullet or number) is controlled just by the // first item at the indent. Subsequent changes are ignored unless they // are for nested lists // lists: (function( ) { // Use a closure to hide a few variables. var any_list = "[*+-]|\\d+\\.", bullet_list = /[*+-]/, // Capture leading indent as it matters for determining nested lists. is_list_re = new RegExp( "^( {0,3})(" + any_list + ")[ \t]+" ), indent_re = "(?: {0,3}\\t| {4})"; // TODO: Cache this regexp for certain depths. // Create a regexp suitable for matching an li for a given stack depth function regex_for_depth( depth ) { return new RegExp( // m[1] = indent, m[2] = list_type "(?:^(" + indent_re + "{0," + depth + "} {0,3})(" + any_list + ")\\s+)|" + // m[3] = cont "(^" + indent_re + "{0," + (depth-1) + "}[ ]{0,4})" ); } function expand_tab( input ) { return input.replace( / {0,3}\t/g, " " ); } // Add inline content `inline` to `li`. inline comes from processInline // so is an array of content function add(li, loose, inline, nl) { if ( loose ) { li.push( [ "para" ].concat(inline) ); return; } // Hmmm, should this be any block level element or just paras? var add_to = li[li.length -1] instanceof Array && li[li.length - 1][0] === "para" ? li[li.length -1] : li; // If there is already some content in this list, add the new line in if ( nl && li.length > 1 ) inline.unshift(nl); for ( var i = 0; i < inline.length; i++ ) { var what = inline[i], is_str = typeof what === "string"; if ( is_str && add_to.length > 1 && typeof add_to[add_to.length-1] === "string" ) add_to[ add_to.length-1 ] += what; else add_to.push( what ); } } // contained means have an indent greater than the current one. On // *every* line in the block function get_contained_blocks( depth, blocks ) { var re = new RegExp( "^(" + indent_re + "{" + depth + "}.*?\\n?)*$" ), replace = new RegExp("^" + indent_re + "{" + depth + "}", "gm"), ret = []; while ( blocks.length > 0 ) { if ( re.exec( blocks[0] ) ) { var b = blocks.shift(), // Now remove that indent x = b.replace( replace, ""); ret.push( mk_block( x, b.trailing, b.lineNumber ) ); } else break; } return ret; } // passed to stack.forEach to turn list items up the stack into paras function paragraphify(s, i, stack) { var list = s.list; var last_li = list[list.length-1]; if ( last_li[1] instanceof Array && last_li[1][0] === "para" ) return; if ( i + 1 === stack.length ) { // Last stack frame // Keep the same array, but replace the contents last_li.push( ["para"].concat( last_li.splice(1, last_li.length - 1) ) ); } else { var sublist = last_li.pop(); last_li.push( ["para"].concat( last_li.splice(1, last_li.length - 1) ), sublist ); } } // The matcher function return function( block, next ) { var m = block.match( is_list_re ); if ( !m ) return undefined; function make_list( m ) { var list = bullet_list.exec( m[2] ) ? ["bulletlist"] : ["numberlist"]; stack.push( { list: list, indent: m[1] } ); return list; } var stack = [], // Stack of lists for nesting. list = make_list( m ), last_li, loose = false, ret = [ stack[0].list ], i; // Loop to search over block looking for inner block elements and loose lists loose_search: while ( true ) { // Split into lines preserving new lines at end of line var lines = block.split( /(?=\n)/ ); // We have to grab all lines for a li and call processInline on them // once as there are some inline things that can span lines. var li_accumulate = "", nl = ""; // Loop over the lines in this block looking for tight lists. tight_search: for ( var line_no = 0; line_no < lines.length; line_no++ ) { nl = ""; var l = lines[line_no].replace(/^\n/, function(n) { nl = n; return ""; }); // TODO: really should cache this var line_re = regex_for_depth( stack.length ); m = l.match( line_re ); //print( "line:", uneval(l), "\nline match:", uneval(m) ); // We have a list item if ( m[1] !== undefined ) { // Process the previous list item, if any if ( li_accumulate.length ) { add( last_li, loose, this.processInline( li_accumulate ), nl ); // Loose mode will have been dealt with. Reset it loose = false; li_accumulate = ""; } m[1] = expand_tab( m[1] ); var wanted_depth = Math.floor(m[1].length/4)+1; //print( "want:", wanted_depth, "stack:", stack.length); if ( wanted_depth > stack.length ) { // Deep enough for a nested list outright //print ( "new nested list" ); list = make_list( m ); last_li.push( list ); last_li = list[1] = [ "listitem" ]; } else { // We aren't deep enough to be strictly a new level. This is // where Md.pl goes nuts. If the indent matches a level in the // stack, put it there, else put it one deeper then the // wanted_depth deserves. var found = false; for ( i = 0; i < stack.length; i++ ) { if ( stack[ i ].indent !== m[1] ) continue; list = stack[ i ].list; stack.splice( i+1, stack.length - (i+1) ); found = true; break; } if (!found) { //print("not found. l:", uneval(l)); wanted_depth++; if ( wanted_depth <= stack.length ) { stack.splice(wanted_depth, stack.length - wanted_depth); //print("Desired depth now", wanted_depth, "stack:", stack.length); list = stack[wanted_depth-1].list; //print("list:", uneval(list) ); } else { //print ("made new stack for messy indent"); list = make_list(m); last_li.push(list); } } //print( uneval(list), "last", list === stack[stack.length-1].list ); last_li = [ "listitem" ]; list.push(last_li); } // end depth of shenegains nl = ""; } // Add content if ( l.length > m[0].length ) li_accumulate += nl + l.substr( m[0].length ); } // tight_search if ( li_accumulate.length ) { add( last_li, loose, this.processInline( li_accumulate ), nl ); // Loose mode will have been dealt with. Reset it loose = false; li_accumulate = ""; } // Look at the next block - we might have a loose list. Or an extra // paragraph for the current li var contained = get_contained_blocks( stack.length, next ); // Deal with code blocks or properly nested lists if ( contained.length > 0 ) { // Make sure all listitems up the stack are paragraphs forEach( stack, paragraphify, this); last_li.push.apply( last_li, this.toTree( contained, [] ) ); } var next_block = next[0] && next[0].valueOf() || ""; if ( next_block.match(is_list_re) || next_block.match( /^ / ) ) { block = next.shift(); // Check for an HR following a list: features/lists/hr_abutting var hr = this.dialect.block.horizRule( block, next ); if ( hr ) { ret.push.apply(ret, hr); break; } // Make sure all listitems up the stack are paragraphs forEach( stack, paragraphify, this); loose = true; continue loose_search; } break; } // loose_search return ret; }; })(), blockquote: function blockquote( block, next ) { if ( !block.match( /^>/m ) ) return undefined; var jsonml = []; // separate out the leading abutting block, if any. I.e. in this case: // // a // > b // if ( block[ 0 ] !== ">" ) { var lines = block.split( /\n/ ), prev = [], line_no = block.lineNumber; // keep shifting lines until you find a crotchet while ( lines.length && lines[ 0 ][ 0 ] !== ">" ) { prev.push( lines.shift() ); line_no++; } var abutting = mk_block( prev.join( "\n" ), "\n", block.lineNumber ); jsonml.push.apply( jsonml, this.processBlock( abutting, [] ) ); // reassemble new block of just block quotes! block = mk_block( lines.join( "\n" ), block.trailing, line_no ); } // if the next block is also a blockquote merge it in while ( next.length && next[ 0 ][ 0 ] === ">" ) { var b = next.shift(); block = mk_block( block + block.trailing + b, b.trailing, block.lineNumber ); } // Strip off the leading "> " and re-process as a block. var input = block.replace( /^> ?/gm, "" ), old_tree = this.tree, processedBlock = this.toTree( input, [ "blockquote" ] ), attr = extract_attr( processedBlock ); // If any link references were found get rid of them if ( attr && attr.references ) { delete attr.references; // And then remove the attribute object if it's empty if ( isEmpty( attr ) ) processedBlock.splice( 1, 1 ); } jsonml.push( processedBlock ); return jsonml; }, referenceDefn: function referenceDefn( block, next) { var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/; // interesting matches are [ , ref_id, url, , title, title ] if ( !block.match(re) ) return undefined; // make an attribute node if it doesn't exist if ( !extract_attr( this.tree ) ) this.tree.splice( 1, 0, {} ); var attrs = extract_attr( this.tree ); // make a references hash if it doesn't exist if ( attrs.references === undefined ) attrs.references = {}; var b = this.loop_re_over_block(re, block, function( m ) { if ( m[2] && m[2][0] === "<" && m[2][m[2].length-1] === ">" ) m[2] = m[2].substring( 1, m[2].length - 1 ); var ref = attrs.references[ m[1].toLowerCase() ] = { href: m[2] }; if ( m[4] !== undefined ) ref.title = m[4]; else if ( m[5] !== undefined ) ref.title = m[5]; } ); if ( b.length ) next.unshift( mk_block( b, block.trailing ) ); return []; }, para: function para( block ) { // everything's a para! return [ ["para"].concat( this.processInline( block ) ) ]; } }, inline: { __oneElement__: function oneElement( text, patterns_or_re, previous_nodes ) { var m, res; patterns_or_re = patterns_or_re || this.dialect.inline.__patterns__; var re = new RegExp( "([\\s\\S]*?)(" + (patterns_or_re.source || patterns_or_re) + ")" ); m = re.exec( text ); if (!m) { // Just boring text return [ text.length, text ]; } else if ( m[1] ) { // Some un-interesting text matched. Return that first return [ m[1].length, m[1] ]; } var res; if ( m[2] in this.dialect.inline ) { res = this.dialect.inline[ m[2] ].call( this, text.substr( m.index ), m, previous_nodes || [] ); } // Default for now to make dev easier. just slurp special and output it. res = res || [ m[2].length, m[2] ]; return res; }, __call__: function inline( text, patterns ) { var out = [], res; function add(x) { //D:self.debug(" adding output", uneval(x)); if ( typeof x === "string" && typeof out[out.length-1] === "string" ) out[ out.length-1 ] += x; else out.push(x); } while ( text.length > 0 ) { res = this.dialect.inline.__oneElement__.call(this, text, patterns, out ); text = text.substr( res.shift() ); forEach(res, add ); } return out; }, // These characters are intersting elsewhere, so have rules for them so that // chunks of plain text blocks don't include them "]": function () {}, "}": function () {}, __escape__ : /^\\[\\`\*_{}\[\]()#\+.!\-]/, "\\": function escaped( text ) { // [ length of input processed, node/children to add... ] // Only esacape: \ ` * _ { } [ ] ( ) # * + - . ! if ( this.dialect.inline.__escape__.exec( text ) ) return [ 2, text.charAt( 1 ) ]; else // Not an esacpe return [ 1, "\\" ]; }, "![": function image( text ) { // Unlike images, alt text is plain text only. no other elements are // allowed in there // ![Alt text](/path/to/img.jpg "Optional title") // 1 2 3 4 <--- captures var m = text.match( /^!\[(.*?)\][ \t]*\([ \t]*([^")]*?)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/ ); if ( m ) { if ( m[2] && m[2][0] === "<" && m[2][m[2].length-1] === ">" ) m[2] = m[2].substring( 1, m[2].length - 1 ); m[2] = this.dialect.inline.__call__.call( this, m[2], /\\/ )[0]; var attrs = { alt: m[1], href: m[2] || "" }; if ( m[4] !== undefined) attrs.title = m[4]; return [ m[0].length, [ "img", attrs ] ]; } // ![Alt text][id] m = text.match( /^!\[(.*?)\][ \t]*\[(.*?)\]/ ); if ( m ) { // We can't check if the reference is known here as it likely wont be // found till after. Check it in md tree->hmtl tree conversion return [ m[0].length, [ "img_ref", { alt: m[1], ref: m[2].toLowerCase(), original: m[0] } ] ]; } // Just consume the '![' return [ 2, "![" ]; }, "[": function link( text ) { var orig = String(text); // Inline content is possible inside `link text` var res = inline_until_char.call( this, text.substr(1), "]" ); // No closing ']' found. Just consume the [ if ( !res ) return [ 1, "[" ]; var consumed = 1 + res[ 0 ], children = res[ 1 ], link, attrs; // At this point the first [...] has been parsed. See what follows to find // out which kind of link we are (reference or direct url) text = text.substr( consumed ); // [link text](/path/to/img.jpg "Optional title") // 1 2 3 <--- captures // This will capture up to the last paren in the block. We then pull // back based on if there a matching ones in the url // ([here](/url/(test)) // The parens have to be balanced var m = text.match( /^\s*\([ \t]*([^"']*)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/ ); if ( m ) { var url = m[1]; consumed += m[0].length; if ( url && url[0] === "<" && url[url.length-1] === ">" ) url = url.substring( 1, url.length - 1 ); // If there is a title we don't have to worry about parens in the url if ( !m[3] ) { var open_parens = 1; // One open that isn't in the capture for ( var len = 0; len < url.length; len++ ) { switch ( url[len] ) { case "(": open_parens++; break; case ")": if ( --open_parens === 0) { consumed -= url.length - len; url = url.substring(0, len); } break; } } } // Process escapes only url = this.dialect.inline.__call__.call( this, url, /\\/ )[0]; attrs = { href: url || "" }; if ( m[3] !== undefined) attrs.title = m[3]; link = [ "link", attrs ].concat( children ); return [ consumed, link ]; } // [Alt text][id] // [Alt text] [id] m = text.match( /^\s*\[(.*?)\]/ ); if ( m ) { consumed += m[ 0 ].length; // [links][] uses links as its reference attrs = { ref: ( m[ 1 ] || String(children) ).toLowerCase(), original: orig.substr( 0, consumed ) }; link = [ "link_ref", attrs ].concat( children ); // We can't check if the reference is known here as it likely wont be // found till after. Check it in md tree->hmtl tree conversion. // Store the original so that conversion can revert if the ref isn't found. return [ consumed, link ]; } // [id] // Only if id is plain (no formatting.) if ( children.length === 1 && typeof children[0] === "string" ) { attrs = { ref: children[0].toLowerCase(), original: orig.substr( 0, consumed ) }; link = [ "link_ref", attrs, children[0] ]; return [ consumed, link ]; } // Just consume the "[" return [ 1, "[" ]; }, "<": function autoLink( text ) { var m; if ( ( m = text.match( /^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/ ) ) !== null ) { if ( m[3] ) return [ m[0].length, [ "link", { href: "mailto:" + m[3] }, m[3] ] ]; else if ( m[2] === "mailto" ) return [ m[0].length, [ "link", { href: m[1] }, m[1].substr("mailto:".length ) ] ]; else return [ m[0].length, [ "link", { href: m[1] }, m[1] ] ]; } return [ 1, "<" ]; }, "`": function inlineCode( text ) { // Inline code block. as many backticks as you like to start it // Always skip over the opening ticks. var m = text.match( /(`+)(([\s\S]*?)\1)/ ); if ( m && m[2] ) return [ m[1].length + m[2].length, [ "inlinecode", m[3] ] ]; else { // TODO: No matching end code found - warn! return [ 1, "`" ]; } }, " \n": function lineBreak() { return [ 3, [ "linebreak" ] ]; } } }; // Meta Helper/generator method for em and strong handling function strong_em( tag, md ) { var state_slot = tag + "_state", other_slot = tag === "strong" ? "em_state" : "strong_state"; function CloseTag(len) { this.len_after = len; this.name = "close_" + md; } return function ( text ) { if ( this[state_slot][0] === md ) { // Most recent em is of this type //D:this.debug("closing", md); this[state_slot].shift(); // "Consume" everything to go back to the recrusion in the else-block below return[ text.length, new CloseTag(text.length-md.length) ]; } else { // Store a clone of the em/strong states var other = this[other_slot].slice(), state = this[state_slot].slice(); this[state_slot].unshift(md); //D:this.debug_indent += " "; // Recurse var res = this.processInline( text.substr( md.length ) ); //D:this.debug_indent = this.debug_indent.substr(2); var last = res[res.length - 1]; //D:this.debug("processInline from", tag + ": ", uneval( res ) ); var check = this[state_slot].shift(); if ( last instanceof CloseTag ) { res.pop(); // We matched! Huzzah. var consumed = text.length - last.len_after; return [ consumed, [ tag ].concat(res) ]; } else { // Restore the state of the other kind. We might have mistakenly closed it. this[other_slot] = other; this[state_slot] = state; // We can't reuse the processed result as it could have wrong parsing contexts in it. return [ md.length, md ]; } } }; // End returned function } Gruber.inline["**"] = strong_em("strong", "**"); Gruber.inline["__"] = strong_em("strong", "__"); Gruber.inline["*"] = strong_em("em", "*"); Gruber.inline["_"] = strong_em("em", "_"); Markdown.dialects.Gruber = Gruber; Markdown.buildBlockOrder ( Markdown.dialects.Gruber.block ); Markdown.buildInlinePatterns( Markdown.dialects.Gruber.inline ); var Maruku = DialectHelpers.subclassDialect( Gruber ), extract_attr = MarkdownHelpers.extract_attr, forEach = MarkdownHelpers.forEach; Maruku.processMetaHash = function processMetaHash( meta_string ) { var meta = split_meta_hash( meta_string ), attr = {}; for ( var i = 0; i < meta.length; ++i ) { // id: #foo if ( /^#/.test( meta[ i ] ) ) attr.id = meta[ i ].substring( 1 ); // class: .foo else if ( /^\./.test( meta[ i ] ) ) { // if class already exists, append the new one if ( attr["class"] ) attr["class"] = attr["class"] + meta[ i ].replace( /./, " " ); else attr["class"] = meta[ i ].substring( 1 ); } // attribute: foo=bar else if ( /\=/.test( meta[ i ] ) ) { var s = meta[ i ].split( /\=/ ); attr[ s[ 0 ] ] = s[ 1 ]; } } return attr; }; function split_meta_hash( meta_string ) { var meta = meta_string.split( "" ), parts = [ "" ], in_quotes = false; while ( meta.length ) { var letter = meta.shift(); switch ( letter ) { case " " : // if we're in a quoted section, keep it if ( in_quotes ) parts[ parts.length - 1 ] += letter; // otherwise make a new part else parts.push( "" ); break; case "'" : case '"' : // reverse the quotes and move straight on in_quotes = !in_quotes; break; case "\\" : // shift off the next letter to be used straight away. // it was escaped so we'll keep it whatever it is letter = meta.shift(); /* falls through */ default : parts[ parts.length - 1 ] += letter; break; } } return parts; } Maruku.block.document_meta = function document_meta( block ) { // we're only interested in the first block if ( block.lineNumber > 1 ) return undefined; // document_meta blocks consist of one or more lines of `Key: Value\n` if ( ! block.match( /^(?:\w+:.*\n)*\w+:.*$/ ) ) return undefined; // make an attribute node if it doesn't exist if ( !extract_attr( this.tree ) ) this.tree.splice( 1, 0, {} ); var pairs = block.split( /\n/ ); for ( var p in pairs ) { var m = pairs[ p ].match( /(\w+):\s*(.*)$/ ), key = m[ 1 ].toLowerCase(), value = m[ 2 ]; this.tree[ 1 ][ key ] = value; } // document_meta produces no content! return []; }; Maruku.block.block_meta = function block_meta( block ) { // check if the last line of the block is an meta hash var m = block.match( /(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/ ); if ( !m ) return undefined; // process the meta hash var attr = this.dialect.processMetaHash( m[ 2 ] ), hash; // if we matched ^ then we need to apply meta to the previous block if ( m[ 1 ] === "" ) { var node = this.tree[ this.tree.length - 1 ]; hash = extract_attr( node ); // if the node is a string (rather than JsonML), bail if ( typeof node === "string" ) return undefined; // create the attribute hash if it doesn't exist if ( !hash ) { hash = {}; node.splice( 1, 0, hash ); } // add the attributes in for ( var a in attr ) hash[ a ] = attr[ a ]; // return nothing so the meta hash is removed return []; } // pull the meta hash off the block and process what's left var b = block.replace( /\n.*$/, "" ), result = this.processBlock( b, [] ); // get or make the attributes hash hash = extract_attr( result[ 0 ] ); if ( !hash ) { hash = {}; result[ 0 ].splice( 1, 0, hash ); } // attach the attributes to the block for ( var a in attr ) hash[ a ] = attr[ a ]; return result; }; Maruku.block.definition_list = function definition_list( block, next ) { // one or more terms followed by one or more definitions, in a single block var tight = /^((?:[^\s:].*\n)+):\s+([\s\S]+)$/, list = [ "dl" ], i, m; // see if we're dealing with a tight or loose block if ( ( m = block.match( tight ) ) ) { // pull subsequent tight DL blocks out of `next` var blocks = [ block ]; while ( next.length && tight.exec( next[ 0 ] ) ) blocks.push( next.shift() ); for ( var b = 0; b < blocks.length; ++b ) { var m = blocks[ b ].match( tight ), terms = m[ 1 ].replace( /\n$/, "" ).split( /\n/ ), defns = m[ 2 ].split( /\n:\s+/ ); // print( uneval( m ) ); for ( i = 0; i < terms.length; ++i ) list.push( [ "dt", terms[ i ] ] ); for ( i = 0; i < defns.length; ++i ) { // run inline processing over the definition list.push( [ "dd" ].concat( this.processInline( defns[ i ].replace( /(\n)\s+/, "$1" ) ) ) ); } } } else { return undefined; } return [ list ]; }; // splits on unescaped instances of @ch. If @ch is not a character the result // can be unpredictable Maruku.block.table = function table ( block ) { var _split_on_unescaped = function( s, ch ) { ch = ch || '\\s'; if ( ch.match(/^[\\|\[\]{}?*.+^$]$/) ) ch = '\\' + ch; var res = [ ], r = new RegExp('^((?:\\\\.|[^\\\\' + ch + '])*)' + ch + '(.*)'), m; while ( ( m = s.match( r ) ) ) { res.push( m[1] ); s = m[2]; } res.push(s); return res; }; var leading_pipe = /^ {0,3}\|(.+)\n {0,3}\|\s*([\-:]+[\-| :]*)\n((?:\s*\|.*(?:\n|$))*)(?=\n|$)/, // find at least an unescaped pipe in each line no_leading_pipe = /^ {0,3}(\S(?:\\.|[^\\|])*\|.*)\n {0,3}([\-:]+\s*\|[\-| :]*)\n((?:(?:\\.|[^\\|])*\|.*(?:\n|$))*)(?=\n|$)/, i, m; if ( ( m = block.match( leading_pipe ) ) ) { // remove leading pipes in contents // (header and horizontal rule already have the leading pipe left out) m[3] = m[3].replace(/^\s*\|/gm, ''); } else if ( ! ( m = block.match( no_leading_pipe ) ) ) { return undefined; } var table = [ "table", [ "thead", [ "tr" ] ], [ "tbody" ] ]; // remove trailing pipes, then split on pipes // (no escaped pipes are allowed in horizontal rule) m[2] = m[2].replace(/\|\s*$/, '').split('|'); // process alignment var html_attrs = [ ]; forEach (m[2], function (s) { if (s.match(/^\s*-+:\s*$/)) html_attrs.push({align: "right"}); else if (s.match(/^\s*:-+\s*$/)) html_attrs.push({align: "left"}); else if (s.match(/^\s*:-+:\s*$/)) html_attrs.push({align: "center"}); else html_attrs.push({}); }); // now for the header, avoid escaped pipes m[1] = _split_on_unescaped(m[1].replace(/\|\s*$/, ''), '|'); for (i = 0; i < m[1].length; i++) { table[1][1].push(['th', html_attrs[i] || {}].concat( this.processInline(m[1][i].trim()))); } // now for body contents forEach (m[3].replace(/\|\s*$/mg, '').split('\n'), function (row) { var html_row = ['tr']; row = _split_on_unescaped(row, '|'); for (i = 0; i < row.length; i++) html_row.push(['td', html_attrs[i] || {}].concat(this.processInline(row[i].trim()))); table[2].push(html_row); }, this); return [table]; }; Maruku.inline[ "{:" ] = function inline_meta( text, matches, out ) { if ( !out.length ) return [ 2, "{:" ]; // get the preceeding element var before = out[ out.length - 1 ]; if ( typeof before === "string" ) return [ 2, "{:" ]; // match a meta hash var m = text.match( /^\{:\s*((?:\\\}|[^\}])*)\s*\}/ ); // no match, false alarm if ( !m ) return [ 2, "{:" ]; // attach the attributes to the preceeding element var meta = this.dialect.processMetaHash( m[ 1 ] ), attr = extract_attr( before ); if ( !attr ) { attr = {}; before.splice( 1, 0, attr ); } for ( var k in meta ) attr[ k ] = meta[ k ]; // cut out the string and replace it with nothing return [ m[ 0 ].length, "" ]; }; Markdown.dialects.Maruku = Maruku; Markdown.dialects.Maruku.inline.__escape__ = /^\\[\\`\*_{}\[\]()#\+.!\-|:]/; Markdown.buildBlockOrder ( Markdown.dialects.Maruku.block ); Markdown.buildInlinePatterns( Markdown.dialects.Maruku.inline ); // Include all our depndencies and; expose.Markdown = Markdown; expose.parse = Markdown.parse; expose.toHTML = Markdown.toHTML; expose.toHTMLTree = Markdown.toHTMLTree; expose.renderJsonML = Markdown.renderJsonML; })(function() { window.markdown = {}; return window.markdown; }()); },{"util":30}]},{},[1]) (1) });