User:Fullstop/autodate.js

// // 1. add //     importScript('User:Fullstop/autodate.js'); //   to your User:yourname/monobook.js if you want to use this. // 2. wrap your dates like this: // /* -- * wpAutoDate is a client-side processor for converting * appropriately-tagged dates into a user-preference based format. * --- * * Function summary: * 1. This function obviates the need to wiki-link dates in order *    for them to be to be reformatted according to date format *    preferences. * 2. It can obtain the date-format preference from the browser, and *    thus also works for anon users, or when an editor has not set *    a date-format preference. * 3. It can supplement a normal wikilinked date too. Just span it *     as appropriate (see next section). * 4. It is a client-side solution for bug #4582 * * How it works: *  The function works by reformatting dates inside s that have *  class="wpAutoDate". When tagged in this fashion, the contents of *   the spans are identifiable as dates and reformatted as appropriate. * *   Although the s can be added manually, the  template *  on en.wiki is also set up to do the necessary tagging. *  ...somedate... also pre-formats dates as 'dmy' (aka "RFC 2822") *  dates. That 'dmy' format is then what a user will see when he/she *  does not have Javascript enabled (or is not running the script). * * How the preferred date-format is inferred: * 1. The function first looks for a global variable named *    'wgUserDateFormat' provided by the server. Like the other *    'wgUserXXX' variables, this is not set if not-logged-in. * 2. Failing option #1, and if logged in, the function will infer *    the date pref from other information provided by the server. *    This information is however only available in normal "view" *    mode, so autodate will cache it in a cookie (expires at end  *     of session) so that it is available in edit preview too. * 3. When options #1 and #2 are not available, the function *    will attempt to infer the date format from the browser's  *     locale. * 4. Failing option #3, the pref will be inferred from the *    timezone. * NB: As of January 2008, the server does not yet provide *    'wgUserDateFormat' (yet). Recognized values would be the *    mediawiki ones: "dmy", "mdy", "ymd" or "ISO 8601". * * Supported formats: * *  This function supports all the date formats settable in userprefs *    and/or that are recognized by /phase3/includes/DateFormatter.php. *    These include: *    .      dmy: e.g. 30 January 2008 or 30 January 2008 BC *. mdy: e.g. January 30, 2008 or January 30, 2008 BC *. ymd: e.g. 2008 January 30 or 2008 BC January 30 *    . ISO 8601 subset: e.g. 2008-01-30 or -2008-01-30 or 0000-01-30 *    .       dm: e.g. 30 January *    .       md: e.g. January 30 * *  Because time/timezone need to be reformatted when converting to/from *    ISO dates, this function supports time/timezone conversion for all *    formats as well. So for example, *    .      2008-01-28 T 22:26:23+02:00 *    .      2008-01-28T22:26:23.95Z *    .      28 January 2008 22:26 UTC+02:00 *    .      January 28, 2008 22:26:23 (UTC) *    .      January 28, 2008 22:26:23 GMT-12 *    .      28 January 22:26:23 UTC+1400 * *  And since wiki-style timestamps are just dates with the time in  *     the "wrong" place, this function will also recognize a *     wiki-style timestamp. For example, *    .      22:26, 28 January 2008 (UTC) * *  Because weekday names are emitted by en:, and because *    leading weekday names were no significant effort to add support for, *    this function can handle those as well. With that, this function *    can also deal with anything that en: can emit. *    So for example, *    .      Monday, January 28, 2008 *    .      Mon, 28 Jan 2008 22:26:23 +0000 * *  In addition, this function can deal with *    .      Every imaginable combination of commas/periods/whitepace. *    .      BCE, AD, and CE in addition the standard BC. * * Date formatting notes: * 1. ISO 8601 defines hyphen and colon separators as optional. In *     this function, as also in DateFormatter.php, they are mandatory. * 2. Following DateFormatter.php's example, when the date format *    preference is ISO 8601 and the source date has no year *    component, the date will be emitted as 'md', i.e. "May, 5". *    Per ISO 8601:2003, negative year values denote a BC year. This *    is supported in both input and output. * 3. ISO 8601 (subset) YYYY-MM-DD dates (with or without optional time *     and timezone information) in the span's title= attribute are *    compatible with the hCalendar microformat. Auto-reformattable *    dates can thus co-exist with hCalendar dates, and for conversion *    the date in the title= attribute will have priority over the *    contents of the span. * 4. Era signifiers (BCE/BC/CE/AD) are normalized. The first date *    with/requiring a signifier determines the suffix for the rest. * 5. Prefix AD (e.g. 'AD 24') is converted to postfix AD ('24 AD'). * 6. CE/AD is always appened to years in the range 1-99. Although *    this is only absolutely necessary for years in the range 1-31, *    it is the author's humble opinion that the other 2-digit dates *    benefit from an era specifier as well. * 7. Abbreviated month names (Jan., Feb. etc) are preserved in their *    abbreviated form. Although MOS:DATE recommends full names, it *     also seems to suggest that there may to be uses for abbreviated *    forms. * 8. Some common non-3-letter abbreviations such as "Thurs" *    or "Sept" are also accepted but normalized to 3-letter forms. * 9. The reformatted dates will have non-breaking spaces between parts *    of a unit. So, "Saturday, February 16 2008 19:27:10.444 UTC+07:00" *    can wrap between "2008" and "19:27" but not anywhere else. * 10. Ordinal suffixes (the 'st', 'nd' in 1st, 2nd, etc) are filtered *    out (per MOS:DATE). The code to emit these in the output (when *     present in the input) is disabled. * * Implementation notes: * 1. The function regulates itself to avoid running for too long *    at a time. This is (in the main) an anti-abuse measure, and *    rather than set a hard limit, it times itself. The timeout is *     such (ca. 0.25 secs) that a page would have to have several *    hundred dates on it before this protection kicks in. The *    function then continuously "reschedules" itself (each time  *     running ca. 0.25 sec) until processing completes. *    The downside of "throttling" is that the page gets redrawn in  *     chunks, which can manifest itself as seemingly unmotivated *    text reflows, which are particularly noticeable when a table *    is redrawn. This functionality is optional but enabled *    by default. * 2. The output format can be selected on a date-for-date basis. This *    functionality was originally to aid debugging, but may be  *     generally useful as well. To override the default date format *    set the span's class to "autodate-xxx" where 'xxx' is one of: *    'dmy', 'mdy', 'ymd' or 'iso'. * 3. Error handling: Dates in an unsupported format, or that have *    other issues (ambiguous year/day values for example) will appear *    in red, with class="error", and with a title= attribute (then  *     seen as a tooltip) that describes the problem. * * See also: *  * HTML 5's tag *         http://www.whatwg.org/specs/web-apps/current-work/#the-time *  * FF 3's microformat API. *         http://developer.mozilla.org/en/docs/Using_microformats

//

function wp_get_datefmt_pref {  // date format is selected as follows...   // 1. from a global 'wgUserDateFormat' if set. // 2. from a cookie set on the last visit to an article //   in normal "view" (non-edit/non-preview) mode. // 3. from the browser's (i.e. operating system's) locale. // 4. from the timezone that the user is in  // always returns a datepref

var s, i, dt;

if (typeof(window.wgUserName) === 'string') { //null if not logged in     var df_list = "|mdy|dmy|ymd|ISO 8601|iso|"; //"ISO 8601" is "iso"

if (typeof(window.wgUserDateFormat) === 'string') { //server provided it? if (df_list.indexOf('|' + window.wgUserDateFormat + '|') !== -1 ) {

//step #1 success! return window.wgUserDateFormat.substr(0,3).toLowerCase; }        //the indexOf would be -1 for unrecognized values (eg "default") }     else { // wgUserDateFormat is not yet implemented

var cname = 'datefmt';

s = null; if (typeof(window.wgAction) === 'string' &&            typeof(window.wgCanonicalSpecialPageName) !== 'string' &&             window.wgAction === 'view') {

ar = document.getElementById("lastmod"); if (ar) { ar = ar.innerHTML; if (/^[^\d]+\d{4}.\d\d.\d\d[^\d]+.*/.test(ar)) { s = "iso"; } else if (/^[^\d]+\d{1,2}[^\d]{1,2}\d{4}[^\d]+.*/.test(ar)) { s = "mdy"; } else if (/^[^\d]+\d{4}[^\d]+\d{1,2}[^\d]+.*/.test(ar)) { s = "ymd"; } else { s = ""; //"dmy" is the default, so fallthrough }         //and use locale/timezone

//update cookie if iso/mdy/ymd //delete cookie if dmy dt = ""; //expires at end of session if (!s) //has already expired (i.e. delete) dt = "; expires=Thu, 1 Jan 1970 00:00:01 GMT"; document.cookie = cname + '=' + s + '; path=/' + dt;

if (s) { window.wgUserDateFormat = s; //cache it                 return s;               } // 's' is "" here }        }            if (s === null && // not in "view"             typeof(document.cookie) === 'string') {

s = ';' + document.cookie + ';'; i = s.indexOf(';' + cname + '='); if (i === -1) i = s.indexOf(' ' + cname + '='); if (i !== -1 && s.charAt(i + cname.length + 5) === ';' &&                          df_list.indexOf('|' + s.substr(                            (i + cname.length + 2), 3 ) + '|') !== -1) {

s = s.substr( (i + cname.length + 2), 3 ); window.wgUserDateFormat = s; //save it for next time return s; // step #2 success! }        } // if (typeof(document.cookie) === 'string')

} // wgUserDateFormat is not yet implemented } // user is logged in (have wgUserName)

s = null; dt = new Date(1999,11-1,22,0,0,0,0); //in dst if (dt) { s = dt.toLocaleString;

if ( s.indexOf('99-11-22') !== -1) { s = 'iso';         // step #3 success } else { var y = s.indexOf('99'); var m = s.indexOf('11'); //can be -1!! var d = s.indexOf('22');

s = null; if (y != -1 && y < d)           s = 'ymd';       // step #3 success else if (d != -1 && d < m)           s = 'dmy';       // step #3 success else if (m != -1 && m < d)           s = 'mdy';       // step #3 success else { i = dt.getTimezoneOffset; //note: 1999-11-22 is in dst

if (i >= ((2*60)+30) && i <= (10*60)) // step #4 success s = 'mdy'; //azores+30 to hawaii (incl.)

//Australian territories begin at 1030 //The following regions will get the wrong defaults // - French Polynesia (1000), should be dmy; // - Am.Samoa & Midway (1100), should be mdy; // - NZ far west territories (1000), should be dmy; }     }      delete dt; }  if (!s) s = 'dmy'; //rest of the world - step #4 success window.wgUserDateFormat = s; //save it until reload return s; }

// -

function _bad_date(elem, oldstr, whatsinv) {  var i, lotsabugs = (/*@cc_on!@*/false);

if (!whatsinv) whatsinv = ''; else whatsinv = ' (' + whatsinv + '?)'; whatsinv = 'Error: malformed date' + whatsinv;

if (typeof(elem.wpAutoDate) === 'undefined') elem.wpAutoDate = true; //flag as been there, done that

if (!lotsabugs && typeof(elem.hasAttribute) !== 'undefined') {

if (elem.childNodes.length === 1 && elem.childNodes[0].nodeType === 3) { elem.childNodes[0].nodeValue = oldstr; oldstr = null; } else if (typeof(elem.textContent) != 'undefined') { elem.textContent = oldstr; oldstr = null; } else if (typeof(elem.innerText) != 'undefined') { elem.innerText = oldstr; oldstr = null; }

if (oldstr === null) { // changed text with grace elem.setAttribute('title', whatsinv); elem.setAttribute('style', 'color:rgb(255,0,0)'); //'#660033';

if (typeof(elem.className) !== 'string') elem.className = "error"; else elem.className += " error"; return;

} // changed text with grace } // if (!lotsabugs && typeof(elem.hasAttribute) != 'undefined')

// have to do it the graceless way oldstr = escape(oldstr); oldstr = oldstr.replace('%20',' ').replace('%2C',','); i = -3; while (-1 !== (i = oldstr.indexOf('%', i+3)) ) oldstr = oldstr.substr(0,i) + '&#' + parseInt(oldstr.substr(i+1,2),16) + ';' + oldstr.substr(i+3); oldstr = '' + oldstr + ' ';

elem.innerHTML = oldstr; return; }

/* - */

var _wpAutoDate_throttling = 1; // 1=enable, 0=disable throttling function wpAutoDate {  /* English language names so that this works even if the * browser's language is something other than English. */  var _mn_l = ["January","February","March","April","May","June","July", "August","September","October","November","December"]; var _mn_s = ["Jan","Feb","Mar","Apr","May","Jun","Jul", "Aug","Sep","Oct","Nov","Dec", "Sept"]; //note "Sept" vs "Sep" var _dn_l = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday", "Saturday"]; var _dn_s = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat", "Tues","Wednes","Thur","Thurs"]; //alt abbrev.

if (typeof(document.getElementById) === 'undefined') return; // DOM not loaded yet if (typeof(document.childNodes) === 'undefined') return; // unsupported implementation

if (arguments.callee.last_idx) { // defined && != 0 //ensure that the function isn't called while a pass is already //in progress (for example, invoked by multiple onload handlers) if (arguments.length < 1) return; if (arguments.callee.last_idx !== parseInt(arguments[0],10)) return; }

try { var datefmt = null; var running_all; var with_xpath; var ar, i, dlist;

var fn_gettext = function(elem) { // caution: this is _not_ general purpose. It makes some // assumptions that shouldn't generally be made.

// IE=625, O=305, FF=219, Webkit=141 (msecs per 500 node reads) if (typeof(elem.textContent) !== 'undefined') { // DOM 3 standard return elem.textContent; } else if ((/*@cc_on!@*/true) // disabled for IE;            && typeof(elem.innerText) !== 'undefined') { //IE swallows \xA0 (&nbsp) added through createTextNode return elem.innerText; } else if (typeof(elem.childNodes) !== 'undefined') { return (function (_el) {              var _s = ''; var _cn = _el.childNodes; var _l = _cn.length;               for (var _i = 0; _i < _l; _i++) {                  switch (_cn[_i].nodeType) {                     case 1: _s += arguments.callee(_cn[_i]); break; //node                     case 3: _s += _cn[_i].nodeValue; break; //text                  }               }               return _s;             })(elem); }         return ''; };

var fn_now = function { if (typeof(Date.now) !== 'function') { var _dt = new Date; var _t = _dt.getTime; delete _dt; // some like this _dt = null; // some like that return _t; }        return Date.now; //ain't spidermonkey cool? };

// NB: the settext operation is the single most // expensive function in the nodewalk loop. // caution: this is _not_ general purpose. It makes some // assumptions that shouldn't generally be made.

var fn_settext = function(elem, s) { if (elem.childNodes.length === 1 && elem.childNodes[0].nodeType===3) elem.childNodes[0].nodeValue = s;        else if (typeof(elem.textContent) !== 'undefined') elem.textContent = s;        else if (typeof(elem.innerText) !== 'undefined') elem.innerText = s;        else {           var _i = -4; var _s = escape(s); _s = _s.replace('%20',' ').replace('%2C',','); while ((_i = _s.indexOf('%', _i+4)) !== -1) _s = _s.substr(0, _i) + '&#' + parseInt(_s.substr(_i+1,2),16) + ';' + _s.substr(_i+3); elem.innerHTML = _s; }        return; };

// simultaneously... // a) load the contents of the span,     // b) check for  linkage, // c) check for nesting.     // This is faster than discrete steps. It is      // also a work around for an IE5 bug (*elem*.getbyTagName // fails to find span's 'A' child nodes)     var fn_3in1 = function (_el, _arp) {         var _s = '';         var _cn = _el.childNodes;          var _l = _cn.length;

for (var _n = 0; _n < _l; _n++) { switch (_cn[_n].nodeType) { case 3: //text //_s += _cn[_n].nodeValue; _s = _s.concat(_cn[_n].nodeValue); break; case 1: { //node //nb: 'i' is outside the namespace if (_cn[_n].nodeName != 'A') { _arp[1] = true; } else { _arp[0] = true; }                 _s = _s.concat(arguments.callee(_cn[_n], _arp)); break; }           }          }         return _s; };

// ++++++++ regularly scheduled programming ++++++

running_all = 0; with_xpath = false;

if (typeof(arguments.callee.coll) === 'object') {

dlist = arguments.callee.coll; datefmt = arguments.callee.last_fmt; with_xpath = !!(document.evaluate);

} else if (document.evaluate) {

// rather like the shadow copy mechanism below, but // leaving array generation to the xpath engine and // pre-filtering on classname

dlist = document.getElementById('bodyContent'); if (dlist) { // As of Jan 2008, webkit can't yet (3.04) do           //   'id("bodyContent")//span[contains(@class,"wpAutoDate")]' dlist = document.evaluate('//span[contains(@class,"wpAutoDate")]',                dlist, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); //XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);           if (dlist) {               if (dlist.snapshotLength) {                  with_xpath = true;                  datefmt = wp_get_datefmt_pref;

arguments.callee.coll = dlist; arguments.callee.last_fmt = datefmt; }           }         }

} else {

dlist = document.getElementById('bodyContent'); if (dlist) { /*@cc_on @if (@_jscript_version <= 5.6)//IE5 or IE6 pre-KB942840 if (typeof(document.all) !== 'undefined') { // workaround IE's crappy GC. Counter-productive for Opera. // Note: this hack only works for *document*.all and directly // (no referencing) on document.all. Referencing or filtering // (i.e. dlist.all and/or .tags("SPAN")) renders the hack // ineffective. Filtering is done separately in the inner loop.

running_all = document.all.length; if (running_all <= (4096*3)) //4096 is the GC threshold, and running_all = 0; // *3 assumes that at most 1/3rd are spans }            @end @*/ if (!running_all) { dlist = dlist.getElementsByTagName('span'); if (dlist) { i = dlist.length; if (!i) { dlist = null; } else if (i >= 100 &&                     (typeof(arguments.callee.last_idx) !== 'number' || arguments.callee.last_idx === 0) ) {

// Shadow copy optimization for Opera and Webkit. // Only marginal gains for Gecko/Spidermonkey.

ar = new Array(i); while (i) { i--; ar[i] = dlist[i]; }                    arguments.callee.coll = ar; dlist = ar; ar = null;

} // use shadow copy } // have spans

} // not running_all

if (dlist) { if (arguments.callee.last_fmt && arguments.callee.last_idx) { datefmt = arguments.callee.last_fmt; } else { datefmt = wp_get_datefmt_pref; arguments.callee.last_fmt = datefmt; }           }

} //have bodyContent } // not using shadow copy

if (datefmt) { // all is well var iso = /^([+-]?\d{4})-(\d{2})-(\d{2})(.*)/; var normalized_time = /^T(\d\d):(\d\d)(:\d\d|)(\.\d+|)(Z\+\d\d:\d\d|Z\-\d\d:\d\d|Z|)$/;

var haslinks = /<a\s+/i; var dclean = /[\s.,]+/g; //update regexen if you change this var ce_mode; var has_buggy_s; var profcntr_1 = 0; var profcntr_2 = 0; var tslice;

var YMd, dMY, dM, MdY, Md; var idx, s, dlist_len; var start_time;

start_time = fn_now; //also used as a "some number" value

//workaround for webkit and IE missing nbsp in \s //\t\r\n also fail for elems created with createTextNode s = '\t\r\n\xA0\u00a0'; s = s.replace(/\s+/g, ''); has_buggy_s = (s.length != 0); //alert("buggy?=" + has_buggy_s + ' "' + escape(s) + '"' );

(function //pseudo "let"         {            // year expressions are intentionally \d+ and not \d{1,4}            // YMd/dMY are intentionally ambiguous for y <= 2 digits            var ymid = '(\\d+\\s*BCE?|' + '\\d+\\s*AD|' + 'AD\\s*\\d+|' + '\\d+\\s*CE|' + '\\d+)[\\s,\\.]+';            var yend = '(\\d+\\s*BCE?[\\s,\\.]+|\\d+\\s*BCE?$|' + '\\d+\\s*AD[\\s,\\.]+|\\d+\\s*AD$|' + 'AD\\s*\\d+[\\s,\\.]+|AD\\s*\\d+$|' + '\\d+\\s*CE[\\s,\\.]+|\\d+\\s*CE$|' + '\\d+[\\s,\\.]+|\\d+$)';            var mmid = '(' + _mn_l.join('|') + '|' +   _mn_s.join('|') + ')[\\s,\\.]+';            var mend = '(' + _mn_l.join('[\\s,\\.]+|') + '[\\s,\\.]+|' +   _mn_l.join('$|') + '$|' +   _mn_s.join('[\\s,\\.]+|') + '[\\s,\\.]+|' +   _mn_s.join('$|') + '$)';            var dmid = '(\\d{1,2})[\\s,\\.]+';            var dend = '(\\d{1,2}[\\s,\\.]+|\\d{1,2}$)';            var tail = '(.*)'; //[\\s,\\.]*(.*)';

s = '^('+_dn_l.join('[\\s,\\.]+|') + '[\\s,\\.]+|' +                     _dn_s.join('[\\s,\\.]+|') + '[\\s,\\.]+|)';

YMd = new RegExp( s + ymid + mmid + dend + tail, 'i' ); dMY = new RegExp( s + dmid + mmid + yend + tail, 'i' ); MdY = new RegExp( s + mmid + dmid + yend + tail, 'i' ); Md = new RegExp( s + mmid + dend +        tail, 'i' ); dM = new RegExp( s + dmid + mend +        tail, 'i' );

delete ymid; delete yend; delete mmid; delete mend; delete dmid; delete dend; delete tail; });

// +++++++++ walk the nodes ++++++++++

if (with_xpath) dlist_len = dlist.snapshotLength; else if (running_all)      // collection of *all* elements dlist_len = running_all; // may change inside the loop else dlist_len = dlist.length;

tslice = 10; ce_mode = ''; idx = 0;

if (arguments.callee.last_idx) { //defined && != 0 if (arguments.callee.last_len === dlist_len) idx = arguments.callee.last_idx; ce_mode = arguments.callee.sav_ce_mode; tslice = arguments.callee.sav_tslice; }        else if (typeof(window._wpAutoDate_throttling) === 'number' ||                  typeof(window._wpAutoDate_throttling) === 'boolean') { if (!(window._wpAutoDate_throttling)) { tslice = 0; //throttling is enabled unless explicitly disabled }        }

for (idx = idx; idx < dlist_len; idx++) {

// other than temps ('ar', 'i' and 's') and 'ce_mode', // all vars decl'd outside the loop are 'const' inside it           var weekday, timestr; var out_datefmt; var short_mname; var ord_suffix; var linkage_needed; var orig_s; var y, m, d;           var elem;

if (with_xpath) {

elem = dlist.snapshotItem(idx);

} else if (!running_all) {

elem = dlist[idx];

} else {          // optimization around crappy GC               if (idx === (dlist_len-1)) {         //length changes when dlist_len = document.all.length; //inserting links } //reading document.all.length is a very expensive operation

elem = document.all[idx]; if (typeof(elem.nodeName) !== 'string') continue; //faster than 'if ("nodeName" in elem)' if (elem.nodeName !== 'SPAN') continue; ar = elem; while (ar && ar !== dlist) ar = ar.parentNode; if (ar !== dlist) continue; }

if (tslice && running_all && elem.wpAutoDate) continue; //already done this

// ++++++++ match classname ++++++++

if (!elem.className) continue; //no classname

s = ' ' + elem.className + ' '; ar = ['wpAutoDate']; y = false; out_datefmt = datefmt; for (d = (ar.length-1); d >= 0; d--) { i = s.indexOf(' ' + ar[d] ); if (i === -1) continue; i += 1 + ar[d].length; if (s.charAt(i) === ' ') { y = true; continue; }              if (d >= 2) continue; if (s.charAt(i) != '-') continue; if ((i + 4) >= s.length) continue; if (s.charAt(i + 4) != ' ') continue; if (("dmy|mdy|ymd|iso".indexOf(s.substr( i + 1, 3)) ) != -1) out_datefmt = s.substr( i + 1, 3 ); y = true; break; }

if (!y) { //not in class if (tslice && running_all) elem.wpAutoDate = true; //flag the element as "done" continue; }

// ++++++++ throttling check ++++++++

// throttling if (tslice               && (profcntr_1 >= tslice)  // not too often               && ((dlist_len - idx) > 5)   ){ // not about to finish

i = fn_now - start_time; profcntr_2++;

d = profcntr_1; //num conversions so far m = 2;       //num conversions till next timeout check if (i > 225) { if (d) { m = (250 / (i / d)) >> 0; // ">>0" to truncate if (m < 1) m = 1; }                 tslice = m; //projected # of iters per 250ms break; //NB: this is the only 'break' in this loop }              if (d) { m = (i / d); // msecs per conversion if (m > 0) m = ((250 - i) / m) >> 0; if (m < 1) // # of conversions in remaining time m = 1; }              tslice = d + m;            }

// +++++ element is going to modified for sure +++++

profcntr_1++; if (tslice && running_all) elem.wpAutoDate = true; //flag the element as "done"

// +++++++ load the span's contents and flags ++++++++

ar = [false, false]; orig_s = fn_3in1(elem, ar); // ~25% of the time is spent here linkage_needed = ar[0];

if (ar[1]) { //contains nested thingies other than .. _bad_date(elem, orig_s, "nested markup"); continue; //typically markup,  or  or something. }

// ++++++++++ load from title= +++++++++++++

if (elem.hasAttribute) { if (elem.hasAttribute('title')) // DOM core s = elem.getAttribute('title'); else s = ''; } else { // non-compliant browser if (typeof(elem.title) == 'string') s = elem.title; else s = ''; }

d = m = y = 0; weekday = timestr = ''; short_mname = false; ord_suffix = false;

if (s.length) { // got title=

orig_s = s; //contents of title= if (has_buggy_s) s = s.replace(/[\xA0\t\r\n\u00a0]/g, ' '); //fallthrough to iso

} else { // ++++++++++ parse contents ++++++++++ //              // no title= means no YYYY-MM-DD override, so                // use the span's contents. //

s = orig_s; //contents of span if (has_buggy_s) s = s.replace(/[\xA0\t\r\n\u00a0]/g, ' ');

if (s.length) {

//normalize wikidate to dMY. if (s.length >= 19 && s.charAt(2) == ':' &&                             s.substr(s.length-5,5) == '(UTC)') { s = s.replace(dclean, ' '); //works because no frac s = s.substr(6, s.length-(6+5)) + s.substr(0,6) + s.substr(s.length-5,5); } else { //strip ordinal suffix if     ((i = s.indexOf('1st')) != -1) i++; else if ((i = s.indexOf('2nd')) != -1) i++; else if ((i = s.indexOf('3rd')) != -1) i++; else if ((i = s.indexOf('th', 1)) <= 0) i = -1; // ,1 disqualifies "thursday" (the only n with 'th') else if (("0123456789".indexOf(s.charAt(i-1))) == -1) i = -1; if (i >= 1) { if (s.length == (i+2) ||                                     /[\.,\s]/.test(s.charAt(i+2)) ) { s = s.substr(0, i) + s.substr(i+2); ord_suffix = true; }                    }                  }

ar = s.match(dMY); if (ar) { d = 2; m = 3; y = 4; } else if (!(!(ar = s.match(YMd)))) { y = 2; m = 3; d = 4; } else if (!(!(ar = s.match(MdY)))) { m = 2; d = 3; y = 4; } else if (!(!(ar = s.match(dM)))) { d = 2; m = 3; } else if (!(!(ar = s.match(Md)))) { m = 2; d = 3; }

if (d) { // got some match

//strip day-field whitespace first. //the field is referred to in Y checking ar[d] = ar[d].replace(dclean, '');

if (y) { // have a year s = ar[y]; if (s.substr(0,2) === 'AD') //leading 'AD' s = s.substr(2).replace(/^\s+/,'') + 'AD';

i = 0; // num digits while (i < s.length &&                              ("0123456789".indexOf(s.charAt(i))) != -1) { i++; }                       if (i === 0 || i > 4) { _bad_date(elem,orig_s,"too many digits in year"); continue; // num is invalid }                        y = parseInt(s.substr(0,i), 10);

if (s.length > i) { //may have AD/BC/CE/BCE s = s.substr(i).replace(dclean, '');

if (s.length) { if (s === 'BCE' || s === 'BC') { y = -y; } else if (s !== 'AD' && s !== 'CE') { _bad_date(elem,orig_s,"'"+s+"'"); continue; // num is invalid }                             if (ce_mode === '') { // mode not determined? if (s.charAt(s.length-1) === 'E') ce_mode = 'BCE'; //found 'bce' or 'ce' else ce_mode = 'BC'; }                             i = 3; //fake the yearlen as >= 3 since // day/year are not ambiguous when year // has ad/ce (bc/bce) qualifier }                       }                        if ((m === 3) // month-field between day and year                           && i < 3 && y > 0 && y <= 31) { //(ymd or dmy) && yearlen < 3 && yearval <= 31 // => day and year fields are ambiguous

i = parseInt( ar[d], 10 ); if (i <= 0 || (i <= 31 && ar[d].length < 3)) { // dayval <= 31 && daylen < 3 digits s = "ambiguous year/day"; if (i <= 0) s = "zero day"; _bad_date(elem, orig_s, s); continue; }                          // year and day need to be swapped ar[d] = y.toString; //set new value for day y = i; //previously loaded day val }

if (y === 0) { _bad_date(elem,orig_s,"zero year/day"); continue; }                    } // have a year field

if (ar[d].length > 2) { //day field > 2 digits? // cosmic rays? _bad_date(elem,orig_s,"too many digits in day"); continue; }                    d = parseInt( ar[d], 10 );

// convert month name to month # s = ar[m].replace(dclean,''); s = s.substr(0,1).toUpperCase + s.substr(1,s.length-1).toLowerCase; m = 0;    // assume error

for (i = 0; i < 12; i++) { if (s === _mn_l[i]) { short_mname = false; m = i+1; break; } else if (s === _mn_s[i]) { short_mname = true; //short_mname is not set when long == short m = i+1; break; }                        }                     if (m === 0) { for (i = 12; i < _mn_s.length; i++) { if (s !== _mn_s[i]) continue; for (i = 0; i < 12; i++) { if (s === _mn_l[i].substr(0, s.length)) { short_mname = true; m = i+1; break; }                          }                           break; }                    }

if (ar[1].length) { //weekday was provided s = ar[1].replace(dclean,''); s = s.substr(0,1).toUpperCase + s.substr(1,s.length-1).toLowerCase;

for (i = 0; i < 7; i++) { if (s === _dn_l[i] || s === _dn_s[i]) break; }                       if (i >= 7) { for (i == (_dn_s.length - 1); i >= 7; i--) { if (s === _dn_s[i]) { for (i = 0; i < 7; i++) { if (s === _dn_l[i].substr(0, s.length) ) break; }                                break; }                          }                        }                        if (i < 7) { if (short_mname && _dn_s[i] !== _dn_l[i]) weekday = _dn_s[i] + '., ' else weekday = _dn_l[i] + ', ' }                    }

//what follows comes last since it trashes 'ar' if (ar[ar.length-1].length) { // have time? s = ar[ar.length-1].replace(/^\s+/g,'');

ar = s.match(                      /^(T\s*|)(\d{1,2}:\d{2}|)(\:\d{2}|)([,\.]\d+|)\s*(.*)/);

if (!ar) { //fallthrough } else if (ar[2].length === 0 && (ar[3].length || ar[4].length || ar[1].length)) { _bad_date(elem,orig_s,"missing hours:minutes"); continue; } else { // normalize timestring as ISO-style // note: its possible that all 3 are "" here if (ar[2].length === 4) //hours needs padding ar[2] = '0' + ar[2]; if (ar[4].charAt(0) === ',') ar[4] = '.' + ar[4].substr(1); if (ar[2].length) // got anything? timestr = 'T' + ar[2] + ar[3] + ar[4]; s = ar[5]; }

if (s.length) { if (s.charAt(0) === '(' && s.charAt(s.length-1) === ')') { s = s.substr(1,s.length-2); }                          ar = s.match(                     /^([A-Za-z\(\)]*|)\s*([\+\-]\d{1,4}|)(\:\d{2}|)\s*$/ );

if (!ar) { //fallthrough } else if (ar[1].length) { i = ar[1].indexOf(')');                             if (i !== -1 && i !== (ar[1].length-1))                                 ar = null;                              else if (ar[1].lastIndexOf('(') > 0) ar = null; else if (i!==-1 && ar[1].indexOf('(')===-1)                                ar = null;                              else if (i===-1 && ar[1].indexOf('(')!==-1) ar = null; else { if (i !== -1) ar[1] = ar[1].substr(1,ar[1].length-2); if (ar[1] !== 'UTC' && ar[1] !== 'GMT' &&                                                     ar[1] !== 'Z') { _bad_date(elem,orig_s,"timezone isn't"+                                                  " 'UTC', 'GMT' or 'Z'"); continue; }                                if (i!==-1 && (ar[2].length||ar[3].length)){ //parenthesized tzname is followed //by something. _bad_date(elem,orig_s,                                       "gunk after '("+ar[1]+")'"); continue; }                             }                            }

if (!ar) { //fallthrough } else if (ar[3].length===0 && ar[2].length===0){ //nothing - no hour and no minute } else if (ar[3].length && ar[2].length === 0) { ar = null; //min but no hour } else if (ar[3].length && ar[2].length >= 4) { ar = null; //hour too long when minute } else if (ar[3].length===0 && ar[2].length>=4) { s = ar[2]; //simple +NNN or +NNNN ar[3] = ':' + s.substr( s.length - 2, 2); ar[2] = s.substr(0, s.length - 2); }

if (!ar) { _bad_date(elem,orig_s,"not a time/timezone"); continue; }                            if (timestr.length === 0) //timezone but no time timestr = 'T00:00';  //implies midnight //normalize the timezone timestr = timestr + 'Z'; // also marks splitpoint if (ar[2].length) { //got an offset too? if (ar[2].length === 2) //hours not '0'-padded ar[2]=ar[2].charAt(0)+'0'+ar[2].charAt(1); if (ar[3].length === 0) ar[3] = ':00'; timestr = timestr + ar[2] + ar[3]; }

} // got tz                    } // have timestr

s = ''; // don't do any more date matching } // matched Md(Y) or dM(Y) or YMd } // anything in the span?

} // have title= or not

// ++++++++++ iso 8901 matching +++++++++++

if (s.length) { // need to try iso

ar = s.replace(/\s+/g, ' ').match(iso); //the replace is needed to catch newlines

if (ar) { if (ar[1].charAt(0) !== '-' && ar[1].charAt(0) !== '+') y = parseInt( ar[1], 10 ); else { y = parseInt( ar[1].substr(1,ar[1].length-1), 10); if (ar[1].charAt(0) === '-') y = -y; }                 m = parseInt( ar[2], 10 ); d = parseInt( ar[3], 10 );

timestr = ar[ar.length-1].replace(/\s+/g,''); if (timestr.length) { //per http://www.w3.org/TR/NOTE-datetime //which is a subset of ISO 8601/EN 28601 ar = timestr.match(        /^(T\d\d:\d\d)(:\d\d|)([\.,]\d+|)(Z|\+\d\d:\d\d|\-\d\d:\d\d|)(.*)/ ); //   1           2      3                 4                5                     if (!ar || (ar && ar[ar.length-1].length !== 0)) { _bad_date(elem,orig_s,'trail not iso'); continue; }                    if (ar[3].charAt(0) === ',') ar[3] = '.' + ar[3].substr(1,ar[3].length-1); if (ar[4].length > 1) { // have tzoff? (not just 'Z') //add 'Z' split point mark for normalized timestr ar[4] = 'Z' + ar[4]; }                    timestr = ar[1] + ar[2] + ar[3] + ar[4]; } // got time s = ''; // don't do any more matching } // iso regex match } // end try iso

// ++++++++++ basic validation of input +++++++++++

if (s.length) { _bad_date(elem,orig_s, "unsupported format"); continue; }           if (m < 1 || m > 12) { _bad_date(elem,orig_s,"month # of range"); continue; }           if (d < 1 || d > 31) { _bad_date(elem,orig_s,"day # out of range"); continue; }           if ((m === 2 && d > 29) || d > (30+ ((m - (m>>3)) & 1))  ) { _bad_date(elem,orig_s,"invalid day for month"); continue; }           if (timestr.length) { // this was originally a check during development // but may as well keep it for live use too ar = timestr.match( normalized_time ); if (ar) { if (parseInt(ar[1], 10) >= 24) ar = null; //bad hours else if (parseInt(ar[2],10) >= 60) ar = null; //bad minutes else if (ar[3].length && parseInt(ar[3].substr(1,2),10)>60) ar = null; //bad seconds else if (ar[3].length && parseInt( ar[3].substr(1,2),10) === 60 &&                        parseInt(ar[2],10) !== 59 &&                         parseInt(ar[2],10) !== 23) ar = null; //leap sec only valid for 23:59 else if (ar[5].length > 1) { //got 'Z+dd:dd' i = parseInt(ar[5].substr(5,2), 10); if (i >= 60) i = 2400; i += 100 * parseInt(ar[5].substr(2,2), 10); if (ar[5].charAt(1) === '-') i = -i; if (i < -1200 || s > 1400) ar = null; }              }               if (!ar) { _bad_date(elem,orig_s,"time/timezone not valid"); continue; }           } //if timestr

// ++++++++++ convert to target format +++++++++++

(function // pseudo-locals            {               //                // The code below is specific to the en.wiki.                // It probably will not work on other wikis.               //               var sm, sd, sy; // string forms of mon/day/year               var mdb, mde, yrb, tzb; // and                var nbsp = '\xA0'; //String.fromCharCode(160); // \u00a0

sd = d.toString;  // day num as string sm = _mn_l[m-1];    // month name (may change below) sy = s = '';        // assume no year

if (y === 0) { if (out_datefmt === 'iso' || out_datefmt === 'ymd') out_datefmt = 'mdy'; } else { if (y < 100) { // for BC date or for disambiguation/clarity if (linkage_needed || out_datefmt !== 'iso') { if (ce_mode === '') { // mode not established yet if (start_time & 1024) // so pick at "random" ce_mode = 'BC'; else ce_mode = 'BCE'; }                       if (y < 0) sy = nbsp + ce_mode; else if (ce_mode.length === 3) sy = nbsp + 'CE'; else sy = nbsp + 'AD'; }                 }                  i = y;                  if (i < 0) { s = '_BC'; i = -i; }                 sy = i.toString + sy;//NNN or 10 BC/AD/BCE/CE s = i.toString + s; // '10' or '10_BC' (link form) }

mdb = mde = yrb = tzb = ''; if (linkage_needed) { if (s) //caution: uses 's' temporary from above yrb = ''; tzb = ''; mdb = ''; mde = ''; }

if (short_mname && (sm !== _mn_s[m-1])) { sm = _mn_s[m-1] + '.'; }

/** disable ordinal suffix output per MOS:DATE if (ord_suffix && (y === 0 || out_datefmt !== 'iso')) { if (d >= 4 && d <= 20) { sd += 'th'; } else { switch (d % 10) { case 1: sd += 'st'; break; case 2: sd += 'nd'; break; case 3: sd += 'rd'; break; default: sd += 'th'; break; }                 }               }               **/

if (timestr) { // have time/timezone i = timestr.indexOf('Z'); //split point mark if (y !== 0 && out_datefmt === 'iso') { if (i !== -1) { // got a 'Z'                       if (i === (timestr.length - 1)) //only trailing Z                           s = tzb + 'Z' + mde; else //got an offset, so discard the 'Z'                          s = timestr.substr(i+1, timestr.length-(i+1)); timestr = timestr.substr(0,i) + s;                    } } else { //non-iso timestr = ' ' + timestr.substr(1, timestr.length-1); // timestr now has ' ' instead of 'T'                    if (i !== -1) { // got a 'Z'                        if (i === (timestr.length - 1)) //only trailing Z                           s = nbsp + '(' + tzb + 'UTC' + mde + ')'; else s = nbsp + tzb + 'UTC' + mde + timestr.substr(i+1, timestr.length-(i+1)); timestr = timestr.substr(0, i) + s;                    } }              }

s = ''; if (out_datefmt === 'dmy') { if (y) sy = nbsp + yrb + sy + mde; s = weekday.concat(mdb, sd, nbsp, sm, mde, sy, timestr); } else if (out_datefmt === 'mdy' || y === 0) { if (y) sy = ',' + nbsp + yrb + sy + mde; s = weekday.concat(mdb, sm, nbsp, sd, mde, sy, timestr); } else if (out_datefmt === 'ymd') { //will have year because y === 0 already checked s = weekday.concat(yrb, sy, mde, nbsp,                                     mdb, sm, nbsp, sd, mde, timestr); } else { // iso //will have year because y === 0 already checked i = (y < 0 ? -y : y); sy = i.toString; if (i < 10) sy = '000' + sy; else if (i < 100) sy = '00' + sy; else if (i < 1000) sy = '0' + sy; if (y < 0) // year is negative sy = '-' + sy; sm = m.toString; if (m < 10) sm = '0' + sm; if (d < 10) sd = '0' + sd; //no weekday s = yrb.concat(sy, mde, '-', mdb, sm, '-', sd, mde,                                 timestr); }

}); // day and month are valid

// 25-30% of the total time is spent here if (linkage_needed) //need linkage? (classname can be "") elem.innerHTML = s;           else fn_settext(elem, s);

} // for (idx = 0; idx < dlist_len; idx++)

if (idx < dlist_len) { //have more to do           arguments.callee.sav_tslice = tslice; arguments.callee.sav_ce_mode = ce_mode; arguments.callee.last_idx = idx; arguments.callee.last_len = dlist_len;

if (false) { if (typeof(arguments.callee.timer) === 'undefined') { arguments.callee.timer = setInterval(                      function{wpAutoDate(wpAutoDate.last_idx);}, 100 ); }           } else { arguments.callee.timer = setTimeout(                      function{wpAutoDate(wpAutoDate.last_idx);}, 100 ); }

} else { // done all

if (typeof(arguments.callee.timer) !== 'undefined') { clearInterval(arguments.callee.timer); delete arguments.callee.timer; }           if (typeof(arguments.callee.coll) === 'object') { delete arguments.callee.coll; }           arguments.callee.last_idx = 0; arguments.callee.last_len = 0; // fin! }

dlist = null;

if (window.autodate_profiling) {

ar = fn_now;

if (!(arguments.callee.num_runs)) { arguments.callee.first_time = ar; arguments.callee.total_time = 0; arguments.callee.num_done = 0; arguments.callee.num_checks = 0; arguments.callee.num_runs = 0; }

start_time = (ar - start_time);

arguments.callee.total_time += start_time; arguments.callee.num_done += profcntr_1; arguments.callee.num_checks += profcntr_2; arguments.callee.num_runs++;

//if (idx >= dlist_len) {              i = profcntr_1; if (i) i = Math.round((start_time * 100)/ i) / 100;

s = 'autodate ' + '(' + arguments.callee.num_runs + ') ' + ((idx >= dlist_len) ? ('ran ') :                                   ('running #' + idx + ' of ') ) + dlist_len; /*              s += ' (last: ' + start_time //time elapsed this round                               + '@' + profcntr_2                               + ' ' + profcntr_1                               + '@' + i                               + ')'; s += ' (totals: '+ (ar - arguments.callee.first_time)                        + '@' + arguments.callee.num_checks                          + ' ' + arguments.callee.num_done                          + '@' + arguments.callee.total_time                         + ')'; */

i = arguments.callee.num_done; if (i) i = Math.round((arguments.callee.total_time * 100)/i)/100;

// in "a@b x@y" ... //   a: average run time per fn call (target is 250ms) //   b: average # of timeout checks per run (target is 1) //   x: average # of conversions per run //   y: average time of each conversion s += ' avg: ' + Math.round((arguments.callee.total_time * 100)/                                          arguments.callee.num_runs) / 100 + '@' + Math.round((arguments.callee.num_checks * 100)/                                          arguments.callee.num_runs) / 100 + ' ' + Math.round((arguments.callee.num_done * 100)/                                          arguments.callee.num_runs) / 100 + '@' + i                      + 'ms';

fn_settext(window.autodate_profiling, s ); }

if (idx >= dlist_len) { arguments.callee.num_runs = 0; arguments.callee.total_time = 0; arguments.callee.num_done = 0; }        } // if (profiling)

} // if (datefmt)

} catch(err) { if (typeof(arguments.callee.timer) !== 'undefined') { clearInterval(arguments.callee.timer); delete arguments.callee.timer; }    if (typeof(arguments.callee.coll) === 'object') { delete arguments.callee.coll; }    arguments.callee.last_idx = 0; arguments.callee.last_len = 0;

//throw(err); //this should be commented out unless debugging }

return; }

if (window.addOnloadHook) { if (typeof(window.wgAction) === 'string' && window.wgAction !== 'edit') addOnloadHook(wpAutoDate); }