Chaturbate Easy Tipping

Adds a new tipping popup and modifies the existing one

  1. // ==UserScript==
  2. // @name Chaturbate Easy Tipping
  3. // @namespace madTipper
  4. // @version 0.1
  5. // @author omgmikey
  6. // @match https://*.chaturbate.com/*
  7. // @require http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
  8. // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @description Adds a new tipping popup and modifies the existing one
  13. // ==/UserScript==
  14. /* jshint -W097 */
  15. 'use strict';
  16.  
  17. CSS_GREY = {'color': 'rgb(88,141,61)'};
  18. CSS_WHITE = {'color': '#FFFFFF'};
  19. CSS_BLACK = {'color': '#000000'};
  20.  
  21. ID_PREFIX = '#madTipper'
  22. CLASS_PREFIX = '.madTipper'
  23. CLASS_INPUT = CLASS_PREFIX + '_input';
  24.  
  25. var HTML_IDS = {
  26. 'BUTTON': 'button',
  27. 'POPUP': 'popup',
  28. 'AMOUNT': 'amount',
  29. 'COUNT': 'count',
  30. 'INTERVAL': 'interval',
  31. 'VARIANCE_LOWER': 'variance_lower',
  32. 'VARIANCE_UPPER': 'variance_upper',
  33.  
  34. 'START': 'start',
  35. 'STOP': 'stop',
  36. 'TOTAL': 'total',
  37. 'ETA': 'eta'
  38. }
  39.  
  40. for (var key in HTML_IDS) {
  41. HTML_IDS[key] = ID_PREFIX + '_' + HTML_IDS[key];
  42. }
  43.  
  44. var shell = $('.tip_shell');
  45. var tipsLeft = 0;
  46. var tipFunctionTimeout = null;
  47. var juration = loadJuration();
  48.  
  49. (function initialize() {
  50.  
  51. createTipperButton();
  52. createTipperPopup();
  53. injectCSS();
  54. improveDefaultTipPopup();
  55.  
  56. loadPreviousSettings();
  57. initializeButtonCallbacks();
  58. updateTipperButton();
  59. })();
  60.  
  61. function createTipperButton() {
  62.  
  63. shell.append('<div id="madTipper_button_bg"><a href="#" id="madTipper_button"></a></div>');
  64. }
  65.  
  66. function updateTipperButton() {
  67.  
  68. if (tipsLeft == 0) {
  69. $(HTML_IDS['BUTTON']).html('MAD TIPPER').css({'width': '80px'});
  70. }
  71. else {
  72. $(HTML_IDS['BUTTON']).html('MAD TIPPER (' + tipsLeft + ')').css({'width': '120px'});
  73. }
  74. }
  75.  
  76. function createTipperPopup() {
  77.  
  78. shell.append(
  79. '<div class="overlay_popup" id="madTipper_popup">' +
  80. '<table width="100%" border="0" cellspacing="0" cellpadding="0">' +
  81. '<tbody>' +
  82. '<tr>' +
  83. '<td class="formborder">' +
  84. '<div class="title">Mad Tipper</div>' +
  85. '<div class="body">' +
  86. '<form>' +
  87. '<label>Amount per tip:</label><br >' +
  88. '<input type="text" id="madTipper_amount" class="madTipper_input">' +
  89. '<br />' +
  90.  
  91. '<label>Number of tips:</label><br >' +
  92. '<input type="text" id="madTipper_count" class="madTipper_input">' +
  93. '<br /><hr />' +
  94.  
  95. '<label>Interval:</label><br >' +
  96. '<input type="text" id="madTipper_interval" class="madTipper_input">' +
  97. '<br />' +
  98.  
  99. '<label>Interval variance lower (optional):</label><br >' +
  100. '<input type="text" id="madTipper_variance_lower" class="madTipper_input">' +
  101. '<br />' +
  102.  
  103. '<label>Interval variance upper (optional):</label><br >' +
  104. '<input type="text" id="madTipper_variance_upper" class="madTipper_input">' +
  105. '<br /><hr />' +
  106.  
  107. 'Total tip: ' + '<a id="madTipper_total"></a>' +
  108. '<br />' +
  109. 'Estimated duration: ' + '<a id="madTipper_eta"></a>' +
  110. '</form>' +
  111. '<hr />' +
  112. '<button id="madTipper_start">Start</button>' +
  113. '<button id="madTipper_stop" disabled="disabled">Stop</button>' +
  114. '</div>' +
  115. '</td>' +
  116. '</tr>' +
  117. '</tbody>' +
  118. '</table>' +
  119. '</div>'
  120. );
  121. }
  122.  
  123. function injectCSS() {
  124.  
  125. var buttonBackgroundUrl =
  126. 'url("https://ssl-ccstatic.highwebmedia.com/images/btn-sprites2.gif?ac5eba7d5cf3") no-repeat right';
  127.  
  128. var buttonFontFamily =
  129. 'UbuntuMedium,Arial,Helvetica,sans-serif';
  130.  
  131. var genericButtonCSS = {
  132. 'height':'21px',
  133. 'width':'100px',
  134. 'padding-left':'10px',
  135. 'margin-right':'10px',
  136. 'font-size':'12px',
  137. 'text-shadow':'1px 1px 0 #588d3d',
  138. 'color': '#FFFFFF'
  139. };
  140.  
  141. genericButtonCSS['font-family'] = buttonFontFamily;
  142. genericButtonCSS['background'] = buttonBackgroundUrl + ' -84px';
  143.  
  144. var mainButtonCSS = {
  145. 'position': 'absolute',
  146. 'left': '500px',
  147. 'top': '30px',
  148. 'height': '18px',
  149. 'padding': '3px 10px 0 0',
  150. 'text-decoration': 'none',
  151. 'text-align': 'center',
  152. 'width': '80px'
  153. }
  154.  
  155. for (var key in genericButtonCSS) {
  156. if (mainButtonCSS[key] === undefined) {
  157. mainButtonCSS[key] = genericButtonCSS[key];
  158. }
  159. }
  160.  
  161. $(HTML_IDS['BUTTON']).css(mainButtonCSS);
  162.  
  163. $(CLASS_INPUT).css({
  164. 'width': 'auto',
  165. 'margin-bottom': '10px'
  166. });
  167.  
  168. $(HTML_IDS['POPUP']).css({
  169. 'position': 'absolute',
  170. 'z-index': 1000,
  171. 'width': '280px',
  172. 'top': '-456px',
  173. 'left': '452px',
  174. 'display': 'none'
  175. }).draggable();
  176.  
  177. $(HTML_IDS['POPUP'] + ' .formborder').css({
  178. 'border-bottom': '2px solid #0b5d81',
  179. 'height': '420px'
  180. });
  181.  
  182. $(HTML_IDS['START']).css(genericButtonCSS);
  183. genericButtonCSS['background'] = buttonBackgroundUrl + ' -42px';
  184.  
  185. delete genericButtonCSS['color'];
  186. $(HTML_IDS['STOP']).css(genericButtonCSS);
  187. }
  188.  
  189. function improveDefaultTipPopup() {
  190.  
  191. $('.overlay_popup.tip_popup').css({
  192. 'top': '-240px'
  193. }).draggable();
  194.  
  195. $('#tip_message').css({
  196. 'margin-bottom': '20px'
  197. })
  198. .append('<input type="checkbox" class="float_right" id="tip_keepopen"></input><br />')
  199. .append('<br /><label class="float_right" for="tip_keepopen">Keep this window open after tipping</label>');
  200.  
  201. $('.float_right').css({
  202. 'float': 'right'
  203. });
  204.  
  205. setPopupHeight('250px');
  206.  
  207. var tipPopup = $('.tip_popup');
  208. var keepOpenCheckbox = $('#tip_keepopen');
  209. var popupIsForcedOpen = false;
  210.  
  211. /* use CB jquery to ensure correct callback execution order */
  212. var tipPopupForm = defchat_settings.domroot.find('.tip_popup form');
  213. tipPopupForm.submit(onFormSubmit);
  214.  
  215. keepOpenCheckbox.css({
  216. 'margin-top': '10px'
  217. });
  218.  
  219. $('body').click(function(ev) {
  220.  
  221. if ($('.tip_button').is(ev.target)) {
  222. popupIsForcedOpen = false;
  223. return;
  224. }
  225.  
  226. if (!popupIsForcedOpen || tipPopup.has(ev.target).length) {
  227. return;
  228. }
  229.  
  230. if (tipPopup.is(':visible')) {
  231. tipPopup.hide();
  232. }
  233.  
  234. popupIsForcedOpen = false;
  235. });
  236.  
  237. function onFormSubmit() {
  238.  
  239. setPopupHeight('270px');
  240.  
  241. if (!keepOpenCheckbox.is(':checked')) {
  242. return;
  243. }
  244.  
  245. if (!tipPopup.is(':visible')) {
  246. tipPopup.show();
  247. popupIsForcedOpen = true;
  248. }
  249. }
  250.  
  251. function setPopupHeight(value) {
  252. $('.overlay_popup.tip_popup .formborder').css({
  253. 'height': value,
  254. });
  255. }
  256. }
  257.  
  258. function startTipping() {
  259.  
  260. var err = verifyTipperFields();
  261.  
  262. if (err) {
  263. alert(err);
  264. stopTipping();
  265. return;
  266. }
  267.  
  268. saveCurrentSettings();
  269.  
  270. $(HTML_IDS['START']).prop('disabled', true).css(CSS_GREY);
  271. $(HTML_IDS['STOP']).prop('disabled', false).css(CSS_WHITE);
  272. $(CLASS_INPUT).prop('disabled', true).css(CSS_GREY);
  273.  
  274. tipsLeft = getTipCount();
  275.  
  276. /* we really want to send the first one immediately */
  277. sendTip();
  278.  
  279. if (tipsLeft > 0) {
  280. chainQueueTips();
  281. }
  282. }
  283.  
  284. function verifyTipperFields() {
  285.  
  286. function isInt(value) {
  287. var regex = /^[0-9]+$/;
  288. return regex.test(String(value));
  289. }
  290.  
  291. function isDuration(value) {
  292. try {
  293. juration.parse(value);
  294. return true;
  295. }
  296. catch(ex) {
  297. return false;
  298. }
  299. }
  300.  
  301. function isDurationOrEmpty(value) {
  302. return value === '' || isDuration(value);
  303. }
  304.  
  305. if (!isInt(getTipAmountRaw()) || getTipAmount() <= 0) {
  306. return 'Tip amount field should be a positive integer.';
  307. }
  308.  
  309. if (!isInt(getTipCountRaw()) || getTipCount() <= 0) {
  310. return 'Tip count field should be a positive integer.';
  311. }
  312.  
  313. if (!isDuration(getTipInterval())) {
  314. return 'Tip interval should contain a duration. E.g.: "2.5s", "1", "2min"';
  315. }
  316.  
  317. if (!isDurationOrEmpty(getVarianceLowerRaw()) || !isDurationOrEmpty(getVarianceUpperRaw())) {
  318. return 'Variance fields should contain durations, or be left blank. E.g.: "", "2.5s"';
  319. }
  320. }
  321.  
  322. function getSleepInterval() {
  323.  
  324. var interval = juration.parse(getTipInterval());
  325. var lower_bound = interval - getVarianceLower();
  326. var upper_bound = interval + getVarianceUpper();
  327.  
  328. return getRandomNumber(lower_bound, upper_bound) * 1000;
  329. }
  330.  
  331. function getRandomNumber(min, max) {
  332.  
  333. return Math.random() * (max - min) + min;
  334. }
  335.  
  336. function chainQueueTips() {
  337.  
  338. var sleepTime = getSleepInterval();
  339.  
  340. tipFunctionTimeout = setTimeout(function() {
  341. sendTip(chainQueueTips);
  342. }, sleepTime);
  343. }
  344.  
  345. function sendTip(queueNextTipFn) {
  346.  
  347. var queryUrl = $('.tip_popup form').attr('action');
  348.  
  349. var queryParams = $.param({
  350. 'csrfmiddlewaretoken': $.cookie('csrftoken'),
  351. 'tip_amount': getTipAmount(),
  352. 'message': '',
  353. 'tip_room_type': $('#id_tip_room_type').val(),
  354. 'tip_v': defchat_settings.v_tip_vol,
  355. });
  356.  
  357. $.ajax({
  358. url: queryUrl,
  359. data: queryParams,
  360. dataType: 'json',
  361. type: 'post',
  362. success: function(response) {
  363. if (response.error) {
  364. alert(response.error);
  365. stopTipping();
  366. }
  367. }
  368. });
  369.  
  370. updateTipsLeft();
  371.  
  372. if (tipsLeft === 0) {
  373. stopTipping();
  374. }
  375. else if (queueNextTipFn) {
  376. queueNextTipFn();
  377. }
  378. }
  379.  
  380. function updateTipsLeft() {
  381.  
  382. tipsLeft--;
  383. updateTipperButton();
  384. }
  385.  
  386. function stopTipping() {
  387.  
  388. clearTimeout(tipFunctionTimeout);
  389. tipFunctionTimeout = null;
  390.  
  391. tipsLeft = 0;
  392. updateTipperButton();
  393.  
  394. $(HTML_IDS['STOP']).prop('disabled', true).css(CSS_GREY);
  395. $(HTML_IDS['START']).prop('disabled', false).css(CSS_WHITE);
  396. $(CLASS_INPUT).prop('disabled', false).css(CSS_BLACK);
  397. }
  398.  
  399. function initializeButtonCallbacks() {
  400.  
  401. var popup = $(HTML_IDS['POPUP']);
  402. var button = $(HTML_IDS['BUTTON']);
  403.  
  404. button.click(function(ev) {
  405. if (popup.is(':visible')) {
  406. popup.hide();
  407. }
  408. else {
  409. popup.show();
  410. }
  411. });
  412.  
  413. popup.click(function(ev) {
  414. ev.stopPropagation();
  415. });
  416.  
  417. $(HTML_IDS['START']).click(function() {
  418. startTipping();
  419. $(HTML_IDS['POPUP']).hide();
  420. });
  421.  
  422. $(HTML_IDS['STOP']).click(function() {
  423. stopTipping();
  424. });
  425.  
  426. $('body').click(function(ev) {
  427. if (ev.target.id != button.prop('id')) {
  428. $(HTML_IDS['POPUP']).hide();
  429. }
  430. });
  431.  
  432. $(CLASS_INPUT).change(function() {
  433. calculateAndSetTotalTip();
  434. calculateAndSetETA();
  435. });
  436. }
  437.  
  438. function calculateAndSetTotalTip() {
  439.  
  440. var value = $(HTML_IDS['AMOUNT']).val() * $(HTML_IDS['COUNT']).val();
  441. $(HTML_IDS['TOTAL']).html(value + ' tokens');
  442. }
  443.  
  444. function calculateAndSetETA() {
  445.  
  446. var interval = juration.parse($(HTML_IDS['INTERVAL']).val());
  447.  
  448. /* we're not counting the first tip */
  449. var count = getTipCount() - 1;
  450.  
  451. var variance_lower = getVarianceLower();
  452. var variance_upper = getVarianceUpper();
  453.  
  454. var eta = (interval + variance_upper - variance_lower) * count;
  455. $(HTML_IDS['ETA']).html(juration.stringify(eta, {'format': 'long', 'units': 2}));
  456. }
  457.  
  458. function getTipAmount() {
  459.  
  460. return parseInt(getTipAmountRaw());
  461. }
  462.  
  463. function getTipAmountRaw() {
  464.  
  465. return $(HTML_IDS['AMOUNT']).val();
  466. }
  467.  
  468. function getTipInterval() {
  469.  
  470. return $(HTML_IDS['INTERVAL']).val();
  471. }
  472.  
  473. function getTipCount() {
  474.  
  475. return parseInt(getTipCountRaw());
  476. }
  477.  
  478. function getTipCountRaw() {
  479.  
  480. return $(HTML_IDS['COUNT']).val();
  481. }
  482.  
  483. function getVarianceLower() {
  484.  
  485. return parseVariance(getVarianceLowerRaw());
  486. }
  487.  
  488. function getVarianceLowerRaw() {
  489.  
  490. return $(HTML_IDS['VARIANCE_LOWER']).val();
  491. }
  492.  
  493.  
  494. function getVarianceUpper() {
  495.  
  496. return parseVariance(getVarianceUpperRaw());
  497. }
  498.  
  499. function getVarianceUpperRaw() {
  500.  
  501. return $(HTML_IDS['VARIANCE_UPPER']).val();
  502. }
  503.  
  504. function parseVariance(variance) {
  505.  
  506. if (variance == '0') {
  507. variance = 0;
  508. }
  509.  
  510. variance = variance || 0;
  511.  
  512. if (variance != 0) {
  513. variance = juration.parse(variance);
  514. }
  515.  
  516. return variance;
  517. }
  518.  
  519. function saveCurrentSettings() {
  520.  
  521. GM_setValue('amount', getTipAmount());
  522. GM_setValue('interval', getTipInterval());
  523. GM_setValue('count', getTipCount());
  524. GM_setValue('variance_lower', getVarianceLower());
  525. GM_setValue('variance_upper', getVarianceUpper());
  526. }
  527.  
  528. function loadPreviousSettings() {
  529.  
  530. var amount = GM_getValue('amount', 1);
  531. $(HTML_IDS['AMOUNT']).val(amount);
  532.  
  533. var count = GM_getValue('count', 10);
  534. $(HTML_IDS['COUNT']).val(count);
  535.  
  536. var interval = GM_getValue('interval', '1s');
  537. $(HTML_IDS['INTERVAL']).val(interval);
  538.  
  539. var variance_lower = GM_getValue('variance_lower', '');
  540. $(HTML_IDS['VARIANCE_LOWER']).val(variance_lower);
  541.  
  542. var variance_upper = GM_getValue('variance_upper', '');
  543. $(HTML_IDS['VARIANCE_UPPER']).val(variance_upper);
  544.  
  545. calculateAndSetTotalTip();
  546. calculateAndSetETA();
  547. }
  548.  
  549.  
  550. // Script ends here
  551. // Libs included because they're not on a popular cdn
  552.  
  553.  
  554. /*
  555. * juration - a natural language duration parser
  556. * https://github.com/domchristie/juration
  557. *
  558. * Copyright 2011, Dom Christie
  559. * Licenced under the MIT licence
  560. *
  561. */
  562.  
  563. function loadJuration() {
  564.  
  565. var UNITS = {
  566. seconds: {
  567. patterns: ['second', 'sec', 's'],
  568. value: 1,
  569. formats: {
  570. 'chrono': '',
  571. 'micro': 's',
  572. 'short': 'sec',
  573. 'long': 'second'
  574. }
  575. },
  576. minutes: {
  577. patterns: ['minute', 'min', 'm(?!s)'],
  578. value: 60,
  579. formats: {
  580. 'chrono': ':',
  581. 'micro': 'm',
  582. 'short': 'min',
  583. 'long': 'minute'
  584. }
  585. },
  586. hours: {
  587. patterns: ['hour', 'hr', 'h'],
  588. value: 3600,
  589. formats: {
  590. 'chrono': ':',
  591. 'micro': 'h',
  592. 'short': 'hr',
  593. 'long': 'hour'
  594. }
  595. },
  596. days: {
  597. patterns: ['day', 'dy', 'd'],
  598. value: 86400,
  599. formats: {
  600. 'chrono': ':',
  601. 'micro': 'd',
  602. 'short': 'day',
  603. 'long': 'day'
  604. }
  605. },
  606. weeks: {
  607. patterns: ['week', 'wk', 'w'],
  608. value: 604800,
  609. formats: {
  610. 'chrono': ':',
  611. 'micro': 'w',
  612. 'short': 'wk',
  613. 'long': 'week'
  614. }
  615. },
  616. months: {
  617. patterns: ['month', 'mon', 'mo', 'mth'],
  618. value: 2628000,
  619. formats: {
  620. 'chrono': ':',
  621. 'micro': 'm',
  622. 'short': 'mth',
  623. 'long': 'month'
  624. }
  625. },
  626. years: {
  627. patterns: ['year', 'yr', 'y'],
  628. value: 31536000,
  629. formats: {
  630. 'chrono': ':',
  631. 'micro': 'y',
  632. 'short': 'yr',
  633. 'long': 'year'
  634. }
  635. }
  636. };
  637.  
  638. var stringify = function(seconds, options) {
  639.  
  640. if(!_isNumeric(seconds)) {
  641. throw "juration.stringify(): Unable to stringify a non-numeric value";
  642. }
  643.  
  644. if((typeof options === 'object' && options.format !== undefined) && (options.format !== 'micro' && options.format !== 'short' && options.format !== 'long' && options.format !== 'chrono')) {
  645. throw "juration.stringify(): format cannot be '" + options.format + "', and must be either 'micro', 'short', or 'long'";
  646. }
  647.  
  648. var defaults = {
  649. format: 'short',
  650. units: undefined
  651. };
  652.  
  653. var opts = _extend(defaults, options);
  654.  
  655. var units = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'], values = [];
  656. var remaining = seconds;
  657. var activeUnits = 0;
  658. for(var i = 0, len = units.length;
  659. i < len && (opts.units == undefined || activeUnits < opts.units);
  660. i++) {
  661. var unit = UNITS[units[i]];
  662. values[i] = Math.floor(remaining / unit.value);
  663. if (values[i] > 0 || activeUnits > 0)
  664. activeUnits++;
  665.  
  666. if(opts.format === 'micro' || opts.format === 'chrono') {
  667. values[i] += unit.formats[opts.format];
  668. }
  669. else {
  670. values[i] += ' ' + _pluralize(values[i], unit.formats[opts.format]);
  671. }
  672. remaining = remaining % unit.value;
  673. }
  674. var output = '';
  675. for(i = 0, len = values.length; i < len; i++) {
  676. if(values[i].charAt(0) !== "0" && opts.format != 'chrono') {
  677. output += values[i] + ' ';
  678. }
  679. else if (opts.format == 'chrono') {
  680. output += _padLeft(values[i]+'', '0', i==values.length-1 ? 2 : 3);
  681. }
  682. }
  683. return output.replace(/\s+$/, '').replace(/^(00:)+/g, '').replace(/^0/, '');
  684. };
  685.  
  686. var parse = function(string) {
  687.  
  688. // returns calculated values separated by spaces
  689. for(var unit in UNITS) {
  690. for(var i = 0, mLen = UNITS[unit].patterns.length; i < mLen; i++) {
  691. var regex = new RegExp("((?:\\d+\\.\\d+)|\\d+)\\s?(" + UNITS[unit].patterns[i] + "s?(?=\\s|\\d|\\b))", 'gi');
  692. string = string.replace(regex, function(str, p1, p2) {
  693. return " " + (p1 * UNITS[unit].value).toString() + " ";
  694. });
  695. }
  696. }
  697.  
  698. var sum = 0,
  699. numbers = string
  700. .replace(/(?!\.)\W+/g, ' ') // replaces non-word chars (excluding '.') with whitespace
  701. .replace(/^\s+|\s+$|(?:and|plus|with)\s?/g, '') // trim L/R whitespace, replace known join words with ''
  702. .split(' ');
  703.  
  704. for(var j = 0, nLen = numbers.length; j < nLen; j++) {
  705. if(numbers[j] && isFinite(numbers[j])) {
  706. sum += parseFloat(numbers[j]);
  707. } else if(!numbers[j]) {
  708. throw "juration.parse(): Unable to parse: a falsey value";
  709. } else {
  710. // throw an exception if it's not a valid word/unit
  711. throw "juration.parse(): Unable to parse: " + numbers[j].replace(/^\d+/g, '');
  712. }
  713. }
  714. return sum;
  715. };
  716.  
  717. // _padLeft('5', '0', 2); // 05
  718. var _padLeft = function(s, c, n) {
  719. if (! s || ! c || s.length >= n) {
  720. return s;
  721. }
  722.  
  723. var max = (n - s.length)/c.length;
  724. for (var i = 0; i < max; i++) {
  725. s = c + s;
  726. }
  727.  
  728. return s;
  729. };
  730.  
  731. var _pluralize = function(count, singular) {
  732. return count == 1 ? singular : singular + "s";
  733. };
  734.  
  735. var _isNumeric = function(n) {
  736. return !isNaN(parseFloat(n)) && isFinite(n);
  737. };
  738.  
  739. var _extend = function(obj, extObj) {
  740. for (var i in extObj) {
  741. if(extObj[i] !== undefined) {
  742. obj[i] = extObj[i];
  743. }
  744. }
  745. return obj;
  746. };
  747.  
  748. var juration = {
  749. parse: parse,
  750. stringify: stringify,
  751. humanize: stringify
  752. };
  753.  
  754. return juration;
  755. };