MediaWiki:LAPI.js: Difference between revisions

Content deleted Content added
No edit summary
per tper
 
(9 intermediate revisions by 4 users not shown)
Line 1:
// <source lang=javascript">
 
/*
Small JS library containing stuff I use often.
Line 24 ⟶ 22:
*/
 
// Global: wgServer, wgScript, wgUserLanguage, injectSpinner, removeSpinner (from wiki.js)
// Global: importScript (from wiki.js, for MediaWiki:AjaxSubmit.js)
 
Line 31 ⟶ 28:
// Remember to double-escape the backslash.
if (typeof (LAPI_file_store) == 'undefined')
var LAPI_file_store = "http(https?:)?//upload\\.wikimedia\\.org/";
 
// Some basic routines, mainly enhancements of the String, Array, and Function objects.
Line 40 ⟶ 37:
// Note: adding these to the prototype may break other code that assumes that
// {} has no properties at all.
if (!Object.clone) {
Object.clone = function (source, includeInherited)
{
if (!source) return null;
var result = {};
for (var key in source) {
if (includeInherited || source.hasOwnProperty (key)) result[key] = source[key];
}
return result;
};
}
 
if (!Object.merge) {
Object.merge = function (from, into, includeInherited)
{
if (!from) return into;
for (var key in from) {
if (includeInherited || from.hasOwnProperty (key)) into[key] = from[key];
}
return into;
};
}
 
if (!Object.mergeSome) {
Object.mergeSome = function (from, into, includeInherited, predicate)
{
if (!from) return into;
if (typeof (predicate) == 'undefined')
return Object.merge (from, into, includeInherited);
for (var key in from) {
if ((includeInherited || from.hasOwnProperty (key)) && predicate (from, into, key))
into[key] = from[key];
}
return into;
};
 
Object.mergeSet = function (from, into, includeInherited)
{
return Object.mergeSome
(from, into, includeInherited, function (src, tgt, key) {return src[key] != null;});
}
 
if (!Object.mergeSet) {
Object.mergeSet = function (from, into, includeInherited)
{
return Object.mergeSome
(from, into, includeInherited, function (src, tgt, key) {return src[key] !== null;});
};
}
/** String enhancements (Javascript 1.6) ************/
 
Line 96 ⟶ 100:
};
}
if (!String.prototype.trimFront)
String.prototype.trimFront = String.prototype.trimLeft; // Synonym
 
// Removes given characters from the end of the string.
Line 106 ⟶ 111:
};
}
if (!String.prototype.trimEnd)
String.prototype.trimEnd = String.prototype.trimRight; // Synonym
 
/** Further String enhancements ************/
 
// Returns true if the string begins with prefix.
if (!String.prototype.startsWith = function (prefix) {
return thisString.indexOfprototype.startsWith = function (prefix) == 0;{
return this.indexOf (prefix) === 0;
};
};
}
 
// Returns true if the string ends in suffix
if (!String.prototype.endsWith = function (suffix) {
String.prototype.endsWith = function (suffix) {
return this.lastIndexOf (suffix) + suffix.length == this.length;
var last = this.lastIndexOf (suffix);
};
 
return last !== -1 && last + suffix.length == this.length;
};
}
 
// Returns true if the string contains s.
if (!String.prototype.contains = function (s) {
String.prototype.contains = function (s) {
return this.indexOf (s) >= 0;
return this.indexOf (s) >= 0;
};
};
}
 
// Replace all occurrences of a string pattern by replacement.
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (pattern, replacement) {
return this.split (pattern).join (replacement);
};
}
 
// Escape all backslashes and single or double quotes such that the result can
// be used in Javascript inside quotes or double quotes.
if (!String.prototype.stringifyJS = function () {
String.prototype.stringifyJS = function () {
return this.replace (/([\\\'\"]|%5C|%27|%22)/g, '\\$1') // ' // Fix syntax coloring
return this.replace (/([\n\\'\"]|%5C|%27|%22)/g, '\\n$1'); // ' // Fix syntax coloring
.replace (/\n/g, '\\n');
};
}
 
// Escape all RegExp special characters such that the result can be safely used
// in a RegExp as a literal.
if (!String.prototype.escapeRE = function () {
String.prototype.escapeRE = function () {
return this.replace (/([\\{}()|.?*+^$\[\]])/g, "\\$1");
return this.replace (/([\\{}()|.?*+^$\[\]])/g, "\\$1");
};
};
}
 
if (!String.prototype.escapeXML = function (quot, apos) {
String.prototype.escapeXML = function (quot, apos) {
var s = this.replace (/&/g, '&amp;')
var s = this.replace (/\xa0&/g, '&nbspamp;')
.replace (/<\xa0/g, '&ltnbsp;')
.replace (/></g, '&gtlt;');
.replace (/>/g, '&gt;');
if (quot) s = s.replace (/\"/g, '&quot;'); // " // Fix syntax coloring
if (apos) s = s.replace (/\'/g, '&apos;'); // ' // Fix syntax coloring
return s;
};
}
 
if (!String.prototype.decodeXML = function () {
String.prototype.decodeXML = function () {
return this.replace(/&quot;/g, '"')
return this.replace(/&aposquot;/g, '"'")
.replace(/&gtapos;/g, '>"'")
.replace(/&ltgt;/g, '<>')
.replace(/&nbsplt;/g, '\xa0<')
.replace(/&ampnbsp;/g, '&\xa0');
.replace(/&amp;/g, '&');
};
};
}
 
if (!String.prototype.capitalizeFirst = function () {
String.prototype.capitalizeFirst = function () {
return this.substring (0, 1).toUpperCase() + this.substring (1);
return this.substring (0, 1).toUpperCase() + this.substring (1);
};
};
}
 
if (!String.prototype.lowercaseFirst = function () {
String.prototype.lowercaseFirst = function () {
return this.substring (0, 1).toLowerCase() + this.substring (1);
return this.substring (0, 1).toLowerCase() + this.substring (1);
};
};
}
 
// This is actually a function on URLs, but since URLs typically are strings in
// Javascript, let's include this one here, too.
if (!String.prototype.getParamValue = function (param) {
String.prototype.getParamValue = function (param) {
var re = new RegExp ('[&?]' + param.escapeRE () + '=([^&#]*)');
var re = new RegExp ('[&?]' + param.escapeRE () + '=([^&#]*)');
var m = re.exec (this);
var m = re.exec (this);
if (m && m.length >= 2) return decodeURIComponent (m[1]);
return null;
};
 
String.getParamValue = function (param, url)
{
if (typeof (url) == 'undefined' || url === null) url = document.___location.href;
try {
return url.getParamValue (param);
} catch (e) {
return null;
};
};
 
if (!String.getParamValue) {
String.getParamValue = function (param, url)
{
if (typeof (url) == 'undefined' || url === null) url = document.___location.href;
try {
return url.getParamValue (param);
} catch (e) {
return null;
}
};
}
 
/** Function enhancements ************/
 
if (!Function.prototype.bind) {
// Return a function that calls the function with 'this' bound to 'thisObject'
// Return a function that calls the function with 'this' bound to 'thisObject'
Function.prototype.bind = function (thisObject) {
Function.prototype.bind = function (thisObject) {
var f = this, obj = thisObject;
var f = this, obj = thisObject, slice = Array.prototype.slice, prefixedArgs = slice.call (arguments, 1);
return function () { return f.apply (obj, arguments); };
return function () { return f.apply (obj, prefixedArgs.concat (slice.call (arguments))); };
};
};
 
}
 
/** Array enhancements (Javascript 1.6) ************/
Line 224 ⟶ 257:
};
}
if (!Array.select)
Array.select = Array.filter; // Synonym
 
// Calls iterator on all elements of the array
Line 261 ⟶ 295:
};
}
if (!Array.forAll)
Array.forAll = Array.every; // Synonym
 
// Returns true if predicate is true for at least one element of the array, false otherwise.
Line 279 ⟶ 314:
};
}
if (!Array.exists)
Array.exists = Array.some; // Synonym
 
// Returns a new array built by applying mapper to all elements.
Line 322 ⟶ 358:
{
if (target === null) return -1;
if (typeof (target.indexOflastIndexOf) == 'function') return target.lastIndexOf (elem, from);
if (typeof (target.length) == 'undefined') return -1;
var l = target.length;
Line 338 ⟶ 374:
/** Additional Array enhancements ************/
 
if (!Array.remove) {
Array.remove = function (target, elem) {
var i = Array.indexOf (target, elem);
if (i >= 0) target.splice (i, 1);
};
}
 
if (!Array.contains) {
Array.contains = function (target, elem) {
return Array.indexOf (target, elem) >= 0;
};
}
 
if (!Array.flatten = function (target) {
Array.flatten = function (target) {
var result = [];
var result = [];
Array.forEach (target, function (elem) {result = result.concat (elem);});
return result;
};
}
 
// Calls selector on the array elements until it returns a non-null object
// and then returns that object. If selector always returns null, any also
// returns null. See also Array.map.
if (!Array.any) {
Array.any = function (target, selector, thisObject)
{
if (target === null) return null;
if (typeof (selector) != 'function')
throw new Error ('Array.any: selector must be a function');
var l = target.length;
var result = null;
if (thisObject) selector = selector.bind (thisObject);
for (var i=0; l && i < l; i++) {
if (i in target) {
result = selector (target[i], i, target);
if (result != null) return result;
}
}
return null;
}
return null};
};
 
// Return a contiguous array of the contents of source, which may be an array or pseudo-array,
// basically anything that has a length and can be indexed. (E.g. live HTMLCollections, but also
// Strings, or objects, or the arguments "variable".
if (!Array.make) = function (source){
Array.make = function (source)
{
{
if (!source || typeof (source.length) == 'undefined') return null;
if (!source || typeof (source.length) == 'undefined') return null;
var result = [];
var l var result = source.length[];
for ( var i=0;l i < l; i++) { = source.length;
for (var i=0; i < l; i++) {
if (i in source) result[result.length] = source[i];
}
return result;
};
}
 
if (typeof (window.LAPI) == 'undefined') {
 
var window.LAPI = {
Ajax :
{
Line 468 ⟶ 514:
if (name || msg) {
if (!asDOM) {
return (
'Exception ' + name + ': ' + msg
+ (file ? '\nFile ' + file + (line ? ' (' + line + ')' : "") : "")
);
} else {
var ex_msg = LAPI.make ('div');
Line 700 ⟶ 746:
parseHTML : function (str, sanity_check)
{
// SimplifiedAlways fromuse thea above,faked fordocument; casesparsing whereas weXML *know*and upthen front thattreating the textresult isas (X)HTML doesn't work right with HTML5.
return LAPI.DOM.fakeHTMLDocument (str);
var doc = null;
if (typeof (DOMParser) != 'undefined') {
var parser = new DOMParser ();
if (parser && parser.parseFromString)
doc = parser.parseFromString (str, 'text/xml');
}
if (!doc || !doc.documentElement || /^parsererror$/i.test (doc.documentElement.tagName)
|| (sanity_check && doc.getElementById (sanity_check) == null))
{
// We had an error, or the sanity check (looking for an element known to be there) failed.
// (Happens on Konqueror 4.2.3/4.2.4 upon the very first call...)
doc = LAPI.DOM.fakeHTMLDocument (str);
}
return doc;
},
 
Line 886 ⟶ 919:
{ // Gecko etc.
if (property == 'cssFloat') property = 'float';
return element.ownerDocument.defaultView.getComputedStyle (element, null).getPropertyValue (property);
return
element.ownerDocument.defaultView.getComputedStyle (element, null).getPropertyValue (property);
} else {
var result;
Line 1,037 ⟶ 1,069:
if (node.nodeName.toLowerCase () == tag) res[res.length] = node;
var curr = node.firstChild;
while (curr) { traverse (curr, tag); curr = curr.nextSibling; }
}
traverse (this.body, tag.toLowerCase ());
Line 1,098 ⟶ 1,130:
if (!file_hist) return result;
try {
var $file_curr = getElementsByClassNamewindow.jQuery ? $(file_hist).find('td.filehistory-selected') : getElementsByClassName(file_hist, 'td', 'filehistory-selected');
// Did they change the column order here? It once was nextSibling.nextSibling... but somehow
// the thumbnails seem to be gone... Right:
// http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/ImagePage.php?r1=52385&r2=53130
file_hist = LAPI.DOM.getInnerText ($file_curr[0].nextSibling);
if (!file_hist.contains ('×')) {
file_hist = LAPI.DOM.getInnerText ($file_curr[0].nextSibling.nextSibling);
if (!file_hist.contains ('×')) file_hist = null;
}
Line 1,131 ⟶ 1,163:
if (!file_div) return null; // Catch page without file...
var imgs = file_div.getElementsByTagName ('img');
title = title || mw.config.get('wgTitle');
for (var i = 0; i < imgs.length; i++) {
var src = decodeURIComponent (imgs[i].getAttribute ('src', 2)).replace ('%26', '&');
if (src.search (new RegExp ('^' + LAPI_file_store + '.*/' + title.replace (/ /g, '_').replace (/(\.svg)$/i, '$1.png').escapeRE () + '(/.*)?$')) == 0)
return imgs[i];
}
Line 1,145 ⟶ 1,177:
var href = lk.getAttribute ('href', 2);
if (!href) return null;
// This is a bit tricky to get right, because wgScript can be a substring prefix of
// First try wgArticlePath: href="/wiki/..."
// wgArticlePath, or vice versa.
var prefix = wgArticlePath.replace ('$1', "");
var script = mw.config.get('wgScript') + '?';
if (!href.startsWith (prefix)) prefix = wgServer + prefix; // Fully expanded URL?
if (href.startsWith (script) || href.startsWith (mw.config.get('wgServer') + script) || mw.config.get('wgServer').startsWith('//') && href.startsWith (document.___location.protocol + mw.config.get('wgServer') + script)) {
// href="/w/index.php?title=..."
return href.getParamValue ('title');
}
// Now try wgArticlePath: href="/wiki/..."
var prefix = mw.config.get('wgArticlePath').replace ('$1', "");
if (!href.startsWith (prefix)) prefix = mw.config.get('wgServer') + prefix; // Fully expanded URL?
if (!href.startsWith (prefix) && prefix.startsWith ('//')) prefix = document.___location.protocol + prefix; // Protocol-relative wgServer?
if (href.startsWith (prefix))
return decodeURIComponent (href.substring (prefix.length));
// Do we have variants?
if (wgVariantArticlePath && wgVariantArticlePath.length > 0) {
var variants = mw.config.get('wgVariantArticlePath');
if (variants && variants.length > 0)
{
var re =
new RegExp (wgVariantArticlePathvariants.escapeRE().replace ('\\$2', "[^\\/]*").replace ('\\$1', "(.*)"));
var m = re.exec (href);
if (m && m.length > 1) return decodeURIComponent (m[m.length-1]);
}
// Finally alternative action paths
// If that doesn't work, try the script entry point
var actions = mw.config.get('wgActionPaths');
if (href.startsWith (wgScript) || href.startsWith (wgServer + wgScript)) {
if (actions) {
// href="/w/index.php?title=..."
for (var i=0; i < actions.length; i++) {
return href.getParamValue ('title');
var p = actions[i];
if (p && p.length > 0) {
p = p.replace('$1', "");
if (!href.startsWith (p)) p = mw.config.get('wgServer') + p;
if (!href.startsWith (p) && p.startsWith('//')) p = document.___location.protocol + p;
if (href.startsWith (p))
return decodeURIComponent (href.substring (p.length));
}
}
}
return null;
},
 
revisionFromHtml : function (htmlOfPage)
{
var revision_id = null;
if (window.mediaWiki) { // MW 1.17+
revision_id = htmlOfPage.match (/(mediaWiki|mw).config.set\(\{.*"wgCurRevisionId"\s*:\s*(\d+),/);
if (revision_id) revision_id = parseInt (revision_id[2], 10);
} else { // MW < 1.17
revision_id = htmlOfPage.match (/wgCurRevisionId\s*=\s*(\d+)[;,]/);
if (revision_id) revision_id = parseInt (revision_id[1], 10);
}
return revision_id;
}
 
Line 1,201 ⟶ 1,266:
// may fail to find elements known to exist.
var doc = null;
// Always use our own parser instead of responseXML; that doesn't work right with HTML5. (It did work with XHTML, though.)
if ( request.responseXML && request.responseXML.documentElement
// if ( request.responseXML && request.responseXML.documentElement.tagName == 'HTML'
// && (!sanity_check || request.responseXML.getElementByIddocumentElement.tagName (sanity_check) !== null)'HTML'
// && (!sanity_check || request.responseXML.getElementById (sanity_check) != null)
)
// { )
// {
doc = request.responseXML;
// doc = request.responseXML;
} else {
// } else {
try {
doc = LAPI.DOM.parseHTML (request.responseText, sanity_check);
Line 1,215 ⟶ 1,281:
doc = null;
}
// }
if (doc) {
try {
Line 1,222 ⟶ 1,288:
if (typeof (failureFunc) == 'function') failureFunc (request, ex);
doc = null;
}
}
if (doc === null) return doc;
// We've gotten XML. There is a subtle difference between XML and (X)HTML concerning leading newlines in textareas:
// XML is required to pass through any whitespace (http://www.w3.org/TR/2004/REC-xml-20040204/#sec-white-space), whereas
// HTML may or must not (e.g. http://www.w3.org/TR/html4/appendix/notes.html#h-B.3.1, though it is unclear whether that
// really applies to the content of a textarea, but the draft HTML 5 spec explicitly says that the first newline in a
// <textarea> is swallowed in HTML:
// http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#element-restrictions).
// Because of the latter MW1.18+ adds a newline after the <textarea> start tag if the value starts with a newline. That
// solves bug 12130 (leading newlines swallowed), but since XML passes us this extra newline, we might end up adding a
// leading newline upon each edit.
// Let's try to make sure that all textarea's values are as they should be in HTML.
// Note: since the above change to always use our own parser, which always returns a faked HTML document, this should be
// unnecessary since doc.isFake should always be true.
if (typeof (LAPI.Ajax.getHTML.extraNewlineRE) == 'undefined') {
// Feature detection. Compare value after parsing with value after .innerHTML.
LAPI.Ajax.getHTML.extraNewlineRE = null; // Don't know; hence do nothing
try {
var testTA = '<textarea id="test">\nTest</textarea>';
var testString = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n'
+ '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" dir="ltr">\n'
+ '<head><title>Test</title></head><body><form>' + testTA + '</form></body>\n'
+ '</html>';
var testDoc = LAPI.DOM.parseHTML (testString, 'test');
var testVal = "" + testDoc.getElementById ('test').value;
if (testDoc.dispose) testDoc.dispose();
var testDiv = LAPI.make ('div', null, {display: 'none'});
document.body.appendChild (testDiv);
testDiv.innerHTML = testTA;
if (testDiv.firstChild.value != testVal) {
LAPI.Ajax.getHTML.extraNewlineRE = /^\r?\n/;
if (testDiv.firstChild.value != testVal.replace(LAPI.Ajax.getHTML.extraNewlineRE, "")) {
// Huh? Not the expected difference: go back to "don't know" mode
LAPI.Ajax.getHTML.extraNewlineRE = null;
}
}
LAPI.DOM.removeNode (testDiv);
} catch (any) {
LAPI.Ajax.getHTML.extraNewlineRE = null;
}
}
if (!doc.isFake && LAPI.Ajax.getHTML.extraNewlineRE !== null) {
// If have a "fake" doc, then we did parse through .innerHTML anyway. No need to fix anything.
// (Hm. Maybe we should just always use a fake doc?)
var tas = doc.getElementsByTagName ('textarea');
for (var i = 0, l = tas.length; i < l; i++) {
tas[i].value = tas[i].value.replace(LAPI.Ajax.getHTML.extraNewlineRE, "");
}
}
Line 1,260 ⟶ 1,374:
}
var method;
if (uri.startsWith ('//')) uri = document.___location.protocol + uri; // Avoid protocol-relative URIs (IE7 bug)
if (uri.length + args.length + 1 < (LAPI.Browser.is_ie ? 2040 : 4080)) {
// Both browsers and web servers may have limits on URL length. IE has a limit of 2083 characters
Line 1,308 ⟶ 1,423:
LAPI.Ajax.getPage = function (page, action, params, success, failure)
{
var uri = mw.config.get('wgServer') + mw.config.get('wgScript') + '?title=' + encodeURIComponent (page)
+ (action ? '&action=' + action : "");
LAPI.Ajax.get (uri, params, success, failure, {overrideMimeType : 'application/xml'});
Line 1,339 ⟶ 1,454:
if (!the_form) throw new Error ('#Server reply does not contain mandatory form.');
}
revision_id = requestLAPI.responseTextWP.matchrevisionFromHtml (/wgCurRevisionId\s*=\s*(\d+)[;,]/request.responseText);
if (revision_id) revision_id = parseInt (revision_id[1], 10);
} catch (ex) {
failureFunc (request, ex);
Line 1,422 ⟶ 1,536:
}
}
var uri = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php' + (action ? '?action=' + action : "");
LAPI.Ajax.get (
uri, params
, function (request, failureFunc) {
if (is_json && request.responseText.trimLeft().charAt (0) != '{') {
failureFunc (request);
} else {
success (
request
, (is_json ? eval ('(' + request.responseText.trimLeft() + ')') : null)
, original_failure
);
Line 1,445 ⟶ 1,559:
if (!success || typeof (success) != 'function')
throw new Error ('No success function supplied for parseWikitext');
if (!wikitext && !on_page)
var params =
throw new Error ('No wikitext or page supplied for parseWikitext');
{ pst : null // Do the pre-save-transform: Pipe magic, tilde expansion, etc.
var params = ,text :null;
if (!wikitext) {
(as_preview ? '\<div style="border:1px solid red; padding:0.5em;"\>'
params = {pst: null, page: on_page};
+ '\<div class="previewnote"\>'
} else {
+ '\{\{MediaWiki:Previewnote/' + (user_language || wgUserLanguage) +'\}\}'
params =
+ '\<\/div>\<div\>\n'
{ pst : null // Do the pre-save-transform: Pipe magic, tilde expansion, : "")etc.
,text + wikitext:
+ (as_preview ? '\<\/div\>\<div style="clearborder:both1px solid red; padding:0.5em;"\>\<\/div\>\<\/div\>' : "")
+ '\<div class="previewnote"\>'
,title: on_page || wgPageName || "API"
+ '\{\{MediaWiki:Previewnote/' + (user_language || mw.config.get('wgUserLanguage')) +'\}\}'
,prop : 'text'
+ '\<\/div>\<div\>\n'
};
: "")
+ wikitext
+ (as_preview ? '\<\/div\>\<div style="clear:both;"\>\<\/div\>\<\/div\>' : "")
,title: on_page || mw.config.get('wgPageName') || "API"
};
}
params.prop = 'text';
params.uselang = user_language || mw.config.get('wgUserLanguage'); // see bugzilla 22764
if (cache && /^\d+$/.test(cache=cache.toString())) {
params.maxage = cache;
Line 1,478 ⟶ 1,600:
}; // end LAPI.Ajax.parseWikitext
 
// Throbber backward-compatibility
LAPI.Ajax.injectSpinner = injectSpinner;
 
LAPI.Ajax.removeSpinner = removeSpinner;
LAPI.Ajax.injectSpinner = function (elementBefore, id) {}; // No-op, replaced as appropriate below.
LAPI.Ajax.removeSpinner = function (id) {}; // No-op, replaced as appropriate below.
 
if (typeof window.jQuery == 'undefined' || typeof window.mediaWiki == 'undefined' || typeof window.mediaWiki.loader == 'undefined') {
// Assume old-stlye
if (typeof window.injectSpinner != 'undefined') {
LAPI.Ajax.injectSpinner = window.injectSpinner;
}
if (typeof window.removeSpinner != 'undefined') {
LAPI.Ajax.removeSpinner = window.removeSpinner;
}
} else {
window.mediaWiki.loader.using('jquery.spinner', function () {
LAPI.Ajax.injectSpinner = function (elementBefore, id) {
window.jQuery(elementBefore).injectSpinner(id);
}
LAPI.Ajax.removeSpinner = function (id) {
window.jQuery.removeSpinner(id);
}
});
}
 
} // end if (guard)
Line 1,915 ⟶ 2,058:
}
, true
, mw.config.get('wgUserLanguage') || null
, mw.config.get('wgPageName') || null
);
return true;
Line 1,968 ⟶ 2,111:
 
} // end if (guard)
 
// </source>